Problem
The how-to guides model aggregates, but the DDD story stops at state: update returns the new instance and nothing else. Real aggregates want to announce what happened — InvoiceIssued, OrganizationRenamed — so the composition root can dispatch side effects (outbox, integration events, projections) without diffing two instances by hand.
Proposal sketch
Stay pure and I/O-free; events are values returned alongside the entity, never dispatched by the package:
const outcome = invoice.update({ status: "ISSUED" as InvoiceStatus });
// today: Result<Invoice, InvalidEntity>
// possible: Result<{ entity: Invoice; events: readonly DomainEvent[] }, InvalidEntity>
Open design questions (all genuinely open — this needs a design pass before any code):
- Where events are declared. A per-entity
events option mapping a name to a payload schema (branded, like everything else)? Or free-form, produced by domain methods in the class body?
- Who produces them. Automatic field-diff events are tempting but wrong (they describe what changed, not why); explicit emission from behaviour methods matches the DDD intent but needs an ergonomic way for a class-body method to both
update and emit.
- Backward compatibility. The return-shape change is breaking; an opt-in surface (
updateWith? a declared-events entity variant?) may be the honest cost of "one concept, one name" — to be weighed against surface growth and the "library can be done" philosophy.
If the design conversation concludes the package should stay state-only, the fallback is a documented pattern: behaviour methods returning Result<{ entity; events }> built from update — which needs no new API, only a how-to page.
Problem
The how-to guides model aggregates, but the DDD story stops at state:
updatereturns the new instance and nothing else. Real aggregates want to announce what happened —InvoiceIssued,OrganizationRenamed— so the composition root can dispatch side effects (outbox, integration events, projections) without diffing two instances by hand.Proposal sketch
Stay pure and I/O-free; events are values returned alongside the entity, never dispatched by the package:
Open design questions (all genuinely open — this needs a design pass before any code):
eventsoption mapping a name to a payload schema (branded, like everything else)? Or free-form, produced by domain methods in the class body?updateand emit.updateWith? a declared-events entity variant?) may be the honest cost of "one concept, one name" — to be weighed against surface growth and the "library can be done" philosophy.If the design conversation concludes the package should stay state-only, the fallback is a documented pattern: behaviour methods returning
Result<{ entity; events }>built fromupdate— which needs no new API, only a how-to page.