Skip to content

Aggregates

Most commands can decide from what they carry plus a projected read model. Some can’t. “Withdraw 200” has to be checked against this account’s actual history, and it has to stay correct when two withdrawals arrive at once — a read model that lags by a few milliseconds will happily approve both.

That is what an aggregate root is for. It rehydrates from the entity’s own event stream, applies new events under its own rules, and commits them as one unit. Where a read model is a snapshot you read, an aggregate root is the thing that decides and records.

[Command]
public record WithdrawFunds([Key] Guid AccountId, decimal Amount)
{
public Task Handle(Account account) => account.Withdraw(Amount);
}

Account is an aggregate root. Arc resolved it for AccountId and replayed its events to rebuild current state before Handle() ran. Whatever the aggregate applies is enrolled in the command’s transaction and committed when the command succeeds — or rolled back when it fails. You never fetch it, never call Commit(), and never touch the event log.

Read modelAggregate root
Answers“what does this look like now?”“is this change allowed, and what happened?”
Built fromevents, materialized to a sinkevents, replayed per command
Consistencyeventualconsistent within the aggregate boundary
Can emit eventsnoyes
Reach for it whengating on projected state, computing inputsan invariant must hold under concurrency

They compose. Validate against a read model to give the user a fast, specific message, and let the aggregate enforce the invariant that actually must not break. See Read models.

The same key resolution that picks a read model picks the aggregate — a [Key] property, a property that converts to EventSourceId, or ICanProvideEventSourceId. See Resolving EventSourceId.

  • Discovered automatically — every type implementing IAggregateRoot is registered without configuration.
  • Resolved per command — the instance is command-scoped and bound to that command’s event source id, rehydrated from its stream on resolution.
  • Committed for you — applied events are enrolled in the command’s transaction and committed on success, rolled back on failure.

If the command carries no usable key, resolution fails with UnableToResolveAggregateRootFromCommandContext.

TopicDescription
Defining an aggregate rootWriting the class itself — applying events, On methods, and how state is rebuilt.
Aggregate roots in commandsTaking one as a Handle() dependency, key resolution, and lifetime.