An aggregate answers one question: what has to be consistent at the same instant? Everything inside the boundary changes together in a single transaction and always satisfies its invariants. Everything outside can be momentarily out of step and catch up shortly.
Getting that boundary right determines both correctness and how well the system handles concurrency, which is why it is the design decision worth the most attention.
What changed in 2026
- Smaller aggregates became the consensus. Repeated experience of contention from large aggregates pushed guidance firmly toward the smallest boundary that preserves genuine invariants.
- Event-driven propagation matured. Reliable mechanisms for updating other aggregates asynchronously made eventual consistency between them more comfortable to adopt.
- The distinction from database design sharpened. Recognition that an aggregate is a domain concept rather than a table structure reduced a common conflation.
- Contention became measurable. Better observability made it easier to see which aggregates were causing lock contention under load.
Sizing the boundary
| Question |
If yes |
| Must these change atomically to preserve a rule? |
Same aggregate |
| Would a brief inconsistency be acceptable? |
Separate aggregates |
| Is this rule enforced by the business or by convenience? |
Only real rules justify the boundary |
| Do many users modify this concurrently? |
Smaller is better; contention scales with size |
| Is this collection unbounded? |
Do not put it inside; it will grow without limit |
The unbounded collection case is the classic mistake. Modelling a customer as an aggregate containing all their orders means loading every order to change anything about the customer, and it means two people placing orders simultaneously contend on the same aggregate. Orders belong in their own aggregate, referencing the customer by identity.
The invariant test is the one that matters. Ask whether a rule genuinely must hold at every instant, or whether it merely feels tidy for things to be updated together. Most apparent invariants are the second kind, and they do not justify the coupling.
Working across aggregates
Reference by identity. An aggregate holding a direct object reference to another invites loading both, changing both, and saving both in one transaction — which merges them into one aggregate in practice regardless of how they are drawn.
Update one aggregate per transaction. When an action must affect two, change the first and emit an event that triggers the second. That accepts a brief window of inconsistency in exchange for avoiding distributed transactions, and it is the whole point of the boundary.
Make the propagation reliable. An event emitted in-process after a commit can be lost if the process dies, which leaves the second aggregate permanently out of step. The outbox pattern is the standard fix, writing the event in the same transaction as the state change.
Handle failure in the downstream update. Eventual consistency means the second update might fail, and a plan for that — retry, then dead letter, as in dead letter queues explained — is what separates eventual consistency from occasional inconsistency.
Common mistakes
- Large aggregates. Contention under concurrency and expensive loads.
- Unbounded collections inside an aggregate. Grows without limit.
- Direct object references between aggregates. Merges them in practice.
- Two aggregates in one transaction. Recreates the coupling the boundary avoided.
- Treating tables as aggregates. They are different concerns; one aggregate may span tables.
- Enforcing convenience as an invariant. Couples things for no real rule.
FAQ
How small should an aggregate be?
As small as the real invariants allow. Frequently a single entity with a few value objects.
What if a rule spans two aggregates?
Enforce it eventually, with compensation when violated. If it genuinely must hold instantly, the boundary is drawn wrong.
Does this require event sourcing?
No. Aggregates and event sourcing are separate ideas that combine well and neither requires the other.
How does this map to microservices?
A service typically owns one or several aggregates, and a bounded context frequently maps to a service. Aggregates are finer-grained than service boundaries.
Where to go next
For discovering boundaries, read event storming guide. For reliable cross-aggregate updates, outbox pattern explained and saga pattern explained.