Entity
Introduction
Section titled “Introduction”Use an entity to model a domain object whose identity remains stable while its state changes. Two orders can have the same properties and still be different orders because they have different identifiers.
The Problem
Section titled “The Problem”Comparing domain objects only by their current properties does not capture their lifecycle. For example, an order’s status or total can change without turning it into a different order.
The Principle
Section titled “The Principle”An entity has a stable identity and encapsulates changeable state. Use its identifier to refer to the same domain object over time; use its behavior to change state while preserving domain rules.
Entities belong in the domain layer. Application services coordinate domain operations, while infrastructure handles persistence and other external concerns.
The Mechanism
Section titled “The Mechanism”Entity<T, TId> is an abstract implementation of IEntity<T, TId>. It provides:
- a readonly
idof typeUniqueId<TId>; getId(), which returns the entity’s identifier;getProps(), which returns a structured clone of the current properties;setProps(), which replaces the properties with a frozen shallow copy.
The constructor is protected, so create concrete entity classes by extending Entity. It accepts initial properties and an optional UUID string. If the ID is omitted or empty, the base class generates one. If a non-empty ID is invalid, construction throws an error. The base class does not define domain validation or entity-specific behavior; the concrete class owns those rules.
Implement an Entity
Section titled “Implement an Entity”This order keeps the same identity while changing its status through a domain operation:
import { Entity } from '@xeno-js/shared'
interface OrderProps { status: 'draft' | 'confirmed' total: number}
export class Order extends Entity<OrderProps, 'Order'> { public constructor(props: OrderProps, id?: string) { super(props, id) }
public confirm(): void { const props = this.getProps()
if (props.status !== 'draft') { throw new Error('Only draft orders can be confirmed.') }
this.setProps({ ...props, status: 'confirmed' }) }}
const order = new Order({ status: 'draft', total: 125 })const originalId = order.id
order.confirm()
console.log(order.getProps().status) // 'confirmed'console.log(order.id === originalId) // trueThe type marker 'Order' keeps this entity’s identifier distinct from other UniqueId kinds at compile time. Keep it aligned with the concrete class name: the base class uses this.constructor.name when creating an ID.
Identity and Existing IDs
Section titled “Identity and Existing IDs”The entity’s id is readonly, and the base class does not provide an operation to replace it. Pass a valid UUID when reconstructing an entity from persisted data:
const restoredOrder = new Order( { status: 'confirmed', total: 125 }, 'c0a80123-4567-489a-8abc-1234567890ab',)
console.log(restoredOrder.id.toString())An omitted or empty ID causes a new UUID to be generated. A non-empty invalid UUID throws during construction rather than silently creating a different identity. Validate external input separately when the calling workflow needs a more specific error.
State and Copies
Section titled “State and Copies”setProps() replaces the entity’s entire property set. The base class freezes a shallow copy of the supplied object, but it does not recursively freeze nested values. getProps() returns a structuredClone of the stored properties, so changes to the returned clone do not update the entity:
const order = new Order({ status: 'draft', total: 125 })const props = order.getProps()
props.total = 200
console.log(order.getProps().total) // 125These methods do not add domain validation. Keep state changes behind methods on the concrete entity so those methods can enforce its invariants. For nested properties, use immutable values or manage their copying and mutation rules in the concrete type.
Entities and Aggregate Roots
Section titled “Entities and Aggregate Roots”Use Entity as the base for domain entities. When the entity is the root of an aggregate and needs aggregate versioning and uncommitted domain-event handling, extend AggregateRoot, which builds on Entity.
Related Documentation
Section titled “Related Documentation”Support Us
Section titled “Support Us”Xeno.JS is an MIT-licensed open source project. It can grow thanks to the support of these awesome people. If you’d like to join them, please read more at support section.
