Skip to content

Aggregates

An order may hold at most 100 items. To enforce that, the command needs the order’s current total, and it needs it to be exact: a read model that lags one event behind could let the 101st item through. An aggregate replays the order’s own events into memory, checks the rule, and applies the new event. Because it knows which revision it replayed, a concurrent change rejects the append instead of breaking the rule.

The API is experimental and part of the optional Chronicle integration. Ordinary Arc commands do not need an event store.

AggregateRead model in a command
State comes fromReplaying the event source’s events on every commandA projection Chronicle stored earlier
FreshnessEvery stored event of the handled typesWhatever the projection has processed
ConcurrencyThe append is rejected when the stream moved after the replayNone; the read model does not lock anything
CostGrows with the number of events in the streamOne lookup
Use it whenA rule depends on exact history of one event sourceThe command needs context, or a rule can tolerate lag

A command can take both. See Read models in commands.

  1. You define a class that extends AggregateRoot and registers a handler per event type.
  2. A command binds it with @inject(commandAggregate(Order)).
  3. Before handle() runs, Arc loads the events for the command’s key, replays them through the handlers, and records the tail it read.
  4. handle() calls methods on the aggregate, which apply() new events.
  5. When the command succeeds, the applied events join the command’s batch, with the recorded tail as the expected revision.
TopicDescription
Defining an aggregate rootHandlers, state, rules, and what the TypeScript aggregate does not have
Injecting into commandsBinding, identity and routing, commit, concurrency, and boundaries