AbstractProtectedconstructorReadonlyidProtected Static ReadonlyabstractMarks a class that exists to be extended and never to be instantiated.
abstract alone cannot do this job: it is erased at emit, and the this
constraint on create is satisfied by the abstract base itself. The
check is Object.hasOwn, so only the class that declares this field for
itself is a base — which also makes the marker available to model authors,
for an abstract intermediate of your own.
StaticcreateBuild an entity of the class this was called on.
const stock = BookStock.create("dune", {
title: "Dune",
barcodes: [],
copies: [],
}); // : BookStock
Creation takes the entity's required properties only. Supplying an optional one does not compile:
BookStock.create(id, { title: "Dune", barcodes: [], copies: [] }); // ok
BookStock.create(id, { title: "Dune", barcodes: [], copies: [],
author: "Herbert" }); // TS2353
That refusal is the point: an entity begins life holding exactly what it
cannot exist without, and everything else arrives through behaviour that
means something in the domain. Read RequiredState for the
?-means-deferred convention this depends on.
Read state, either whole or one property at a time.
Reading is not a write channel: the value handed back is detached from the
state, so mutating it reaches nothing. The copy is one level deep — see
@domain-toolkit/state for that boundary and its one deliberate limit.
Read state, either whole or one property at a time.
Reading is not a write channel: the value handed back is detached from the
state, so mutating it reaches nothing. The copy is one level deep — see
@domain-toolkit/state for that boundary and its one deliberate limit.
ProtectedsetWrite one property. protected, so state changes belong to the entity's
own behaviour and mean something in the domain.
ProtectedrunOpen a mutation context for the duration of operation.
Separate from mutate because rehydration needs the context without
the invariant check: AggregateRoot.replay reaches set through its
handlers, and a per-event assertion would reject valid histories.
ProtectedassertRefuse a state change made outside a domain operation.
This is what makes mutate mandatory rather than a convention: a change made outside an operation is one nothing will check and nothing can roll back.
ProtectedmutateRun one domain operation atomically: either it completes with this object's invariants intact, or nothing about it happened.
addCopy(barcode: string): void {
this.mutate(() => this.apply(new CopyAdded(barcode)));
}
On the way out it calls assertInvariants. If that throws — or if the operation itself throws — the state is restored and every event the operation recorded is discarded, so a caller who catches the error is telling the truth when they assume nothing happened.
One limit, pinned by a test: rollback covers this object's own state and
event buffer, and does not rewind events recorded by child entities
during the operation. Their buffers are #private to each Entity.
Assert every rule this object is responsible for.
A no-op here, and abstract on AggregateRoot. An entity that is not an
aggregate root has no invariants of its own — the rules that span a cluster
belong to the root that owns the boundary — but mutate lives on this
class and needs something to call.
Identity equality. Note what is not compared: none of the attributes.
Two Copy instances loaded separately from the repository, one of them
stale, are still the same copy.
Identity is only meaningful between entities of the same kind: a Copy
and a Member that happen to carry the same string do not refer to the
same thing. "Same kind" is the same concrete class, not instanceof, so
that the answer is symmetric — a.equals(b) and b.equals(a) always
agree, which a subclass-tolerant check cannot promise.
ProtectedrecordAppend an event to this entity's private buffer.
protected on purpose: only the entity's own behaviour may record what
happened to it. Application services cannot fabricate history from outside.
Drain the buffer. Called by the aggregate root — see the class comment.
Draining rather than copying is deliberate: an event must be dispatched exactly once. If a root is saved twice, the second save must not replay history.
ProtectedsnapshotTake a restorable copy of this entity's state, detached on the way out so it does not change under the operation it is protecting.
protected and deliberately not part of the public surface: this exists for
Entity.mutate, not for callers to take savepoints with.
ProtectedrestoreProtectedmarkHow many events this entity has buffered — a mark to rewind to.
Protectedrewind
An Entity is a domain object defined by who it is, not by what it holds.
Its attributes change over their lifetime — a copy of a book gets rebound, relabelled, moved to another shelf — and it remains the same copy throughout. That thread of continuity is the entity. Equality therefore compares identity and nothing else.
Identity is a
stringAn identity is a fact about the entity, not an object of its own. What the concept demands is a stable id and identity-based equality; it says nothing about the id's shape. So
idis a plain, non-blankstring, the entity itself answers equals, and how an id is made is the business of a DomainIdGenerator, never of the entity or its factory.An identity that grows a rule of its own — a prefix someone reads, a time part, a tenant scope — is no longer a mere identity. It has structure, and structure is what a ValueObject is for. Reach for one on that day, for that id, and not before.
Entities record events; they do not publish them
Every Entity can
record()a domain event into a private buffer. Nothing else. There is no bus here, no publish method, no way out.The only exit is AggregateRoot.pullDomainEvents, which drains this buffer along with those of every child entity the root declares. A child entity that records an event and is not reachable from its root's
childEntities()will simply never have that event dispatched — silently.An entity may describe what happened to it, because it is the only object that knows; but the aggregate root decides what leaves the boundary, because it is the only object that knows whether the change was consistent.