Appearance
Aggregate
Grouping entities and value objects into one unit of consistency, with a single root as the only way in
Related Concepts: Domain Modeling | Repository | Domain Service | Module Decomposition
What is an Aggregate?
"An AGGREGATE is a cluster of associated objects that we treat as a unit for the purpose of data changes. Each AGGREGATE has a root and a boundary. The boundary defines what is inside the AGGREGATE. The root is a single, specific ENTITY contained in the AGGREGATE."
Eric Evans, Domain-Driven Design
An aggregate is a group of domain objects that change together. One entity in the group is the aggregate root. Code outside the aggregate holds a reference to the root and nothing else. Every change to any object inside the boundary goes through a method on the root, and the root is responsible for the rules that hold across the group.
A purchase order is the standard example. The PurchaseOrder entity is the root. Its line items are entities that live inside the boundary, and the money amounts on each line are value objects. Outside code never holds a LineItem. It asks the purchase order to add one, and the purchase order decides whether that is allowed.
Aggregates are the tactical pattern that the rest of a domain model is organized around. Repositories load and save aggregates. Domain Services exist for the operations that span more than one aggregate. Module boundaries follow aggregate boundaries. A domain model without clear aggregates has nowhere to put its rules, and the rules end up scattered across whichever callers happen to touch the data.
The Root and Its Members
The aggregate root is an entity with a global identity. It is the object a repository returns, the object a use case calls methods on, and the object other aggregates refer to by ID.
The members inside the boundary are entities and value objects that have no meaning on their own. A line item is only a line item of some purchase order. Member entities have a local identity that is meaningful inside the aggregate and nowhere else. Value objects have no identity at all.
Evans sets out how the root owns its members:
"Choose one entity to be the root of each aggregate, and allow external objects to hold references to the root only (references to internal members passed out for use within a single operation only). Define properties and invariants for the aggregate as a whole and give enforcement responsibility to the root or some designated framework mechanism."
Eric Evans, Domain-Driven Design Reference
In practice that means:
- Outside code holds references to the root only. A use case keeps a
PurchaseOrderin hand, and it is the purchase order it loads, calls, and saves. - The root can pass an internal member out, and the caller may use it within a single operation. A use case can ask the purchase order for a line item, read its quantity, and hand it to a formatter. What the caller must never do is keep the reference past the end of the operation, store it in another object, or change it. A held reference goes stale the moment the root changes, and a mutated member bypasses every rule the root exists to enforce.
- Only the root is loaded directly. Repositories and queries return aggregate roots. A line item reaches a caller by traversal from its purchase order and by no other path.
- The root enforces every invariant on every change. A rule about line items is a method on the purchase order.
- Removing the root removes everything inside the boundary. Line items have no life after their purchase order is gone.
The parenthetical in the Evans quote is the part that gets lost. The rule is a constraint on what callers do with internal members, and it permits more than a strict "never see the inside" reading. Returning members as read-only values, as the items getter below does, is one way to make the constraint easy to honor.
Rules Enforced by the Root
The reason to route changes through the root is that the root is the one place that can see the whole picture. Consider the rule that line items can only change while a purchase order is still a draft. Without an aggregate, that rule lives in every caller:
typescript
// The rule is the caller's problem, and every caller has to remember it
class AddLineItemUseCase {
async execute(poId: string, product: Product, quantity: number) {
const po = await this.purchaseOrders.findById(poId);
if (po.status !== 'draft') {
throw new Error('Cannot modify a submitted purchase order');
}
po.lineItems.push({ productId: product.id, quantity, unitPrice: product.price });
await this.purchaseOrders.save(po);
}
}
class RemoveLineItemUseCase {
async execute(poId: string, lineItemId: string) {
const po = await this.purchaseOrders.findById(poId);
// This caller forgot the rule
po.lineItems = po.lineItems.filter((item) => item.id !== lineItemId);
await this.purchaseOrders.save(po);
}
}With an aggregate, the rule lives on the root and the callers have nothing to forget:
typescript
class PurchaseOrder {
private constructor(
private readonly id: PurchaseOrderId,
private status: PurchaseOrderStatus,
private readonly vendorId: VendorId,
private lineItems: LineItem[]
) {}
addLineItem(product: Product, quantity: Quantity): void {
this.assertDraft();
this.lineItems.push(LineItem.create(product.id, quantity, product.price));
}
removeLineItem(lineItemId: LineItemId): void {
this.assertDraft();
this.lineItems = this.lineItems.filter((item) => !item.id.equals(lineItemId));
}
submit(): void {
this.assertDraft();
if (this.lineItems.length === 0) {
throw new DomainInvariantViolation('A purchase order needs at least one line item');
}
this.status = PurchaseOrderStatus.Submitted;
}
get items(): ReadonlyArray<LineItem> {
return this.lineItems;
}
private assertDraft(): void {
if (this.status !== PurchaseOrderStatus.Draft) {
throw new DomainInvariantViolation('Only draft purchase orders can be changed');
}
}
}The use cases shrink to loading the aggregate, calling one method, and saving. The rule is written once, and the model is incapable of reaching a state the rule forbids.
The Consistency Boundary
The aggregate boundary is a consistency boundary. Everything inside the boundary is consistent at the end of every operation, and a single transaction changes at most one aggregate. Everything outside the boundary is allowed to lag.
This is the decision that gives aggregates their meaning. Two objects that must agree after every single change belong in the same aggregate. Two objects that can disagree for a moment belong in different aggregates, and the model coordinates them some other way.
The one-aggregate-per-transaction rule is a modeling constraint before it is a performance constraint. When a transaction reaches across aggregates, the model is claiming that those objects must be consistent at every instant, and that claim is usually false. A purchase order and the vendor it names do not need to change in lockstep. Making them one aggregate, or saving both in one transaction, locks records that have nothing to do with each other and hides the real shape of the domain.
Designing the Boundary
Vaughn Vernon's three-part essay Effective Aggregate Design is the standard reference on where to draw aggregate boundaries. It comes down to four rules.
Model true invariants in consistency boundaries. Start from the business rules that must hold at all times. An invariant that spans several objects pulls those objects into the same aggregate. An object that no invariant mentions does not belong inside.
Design small aggregates. The smallest aggregate that protects the real invariants is the right one. Vernon's cautionary example is a Product that owned every backlog item, release, and sprint, which failed as soon as two people edited different parts of the same product at once. Most aggregates are a root entity with a handful of value objects.
Reference other aggregates by identity. A purchase order holds a VendorId, never a Vendor object. Holding the ID keeps the vendor outside the purchase order's boundary, makes it impossible to change a vendor through a purchase order by accident, and keeps the aggregate cheap to load.
Use eventual consistency outside the boundary. When a change to one aggregate requires a change to another, the first aggregate publishes a domain event and the second reacts to it. The two are consistent soon, and the model stays honest about the fact that they do not have to be consistent now.
The ordering matters. The first rule decides what goes inside. The other three keep the boundary from growing past what the first rule requires.
When to Use an Aggregate
Every entity a repository loads and saves is already an aggregate root, even when the aggregate contains only that one entity. The question is never whether to have aggregates. The question is where each boundary goes.
Pull an object inside an existing aggregate when:
- A business rule spans the root and that object. A purchase order's total depends on its line items, so the line items are inside.
- The object has no identity outside the root. A line item of no purchase order is meaningless.
- The object is always loaded and saved with the root. Nothing ever asks for a line item by itself.
Keep an object as a separate aggregate when:
- It has its own lifecycle and its own rules. A vendor exists before any purchase order and after all of them are closed.
- Other aggregates refer to it. Several purchase orders name the same vendor.
- The two can disagree for a moment without harm. A vendor's name change does not need to appear on every open purchase order in the same transaction.
If a change to a single business concept needs to touch several aggregates in one transaction, treat that as a signal. Either the boundary is in the wrong place or the consistency requirement is weaker than it looks.
Cross-Aggregate Operations
Operations that span aggregates still happen. The model has two homes for them.
A Domain Service holds the rule when the rule itself belongs to neither aggregate. Transferring funds between two accounts is the standard example. The transfer rules belong to the transfer, and the service debits one account aggregate and credits the other.
A domain event carries the consequence when one aggregate's change has effects elsewhere. When a purchase order is approved, the department's budget records the commitment. The purchase order publishes PurchaseOrderApproved and the budget aggregate handles it in its own transaction. The purchase order knows nothing about budgets, and approval does not fail because the budget was locked.
In both cases the Use Case coordinates loading and saving, and each save is still one aggregate in one transaction.
Aggregates and Domain Modeling
The building blocks of a domain model are entities, value objects, invariants, and aggregates. Entities and value objects are the vocabulary. Invariants are the rules. Aggregates are the structure that decides which object is responsible for which rule.
That makes the aggregate the center of the tactical patterns:
- Repositories exist one per aggregate root. They load and save a whole aggregate, and they never return a member entity on its own.
- Domain Services exist for the operations that an aggregate cannot own because the operation spans more than one.
- Use Cases load an aggregate, call a method on its root, and save it. The business rule is inside the aggregate, and the use case is thin.
- Modules own aggregates. A module boundary that cuts through an aggregate is wrong, and an aggregate that needs two modules to change is a sign the modules are drawn in the wrong place.
The anemic domain model is what a system looks like when it has entities and no aggregates. The objects exist, the rules live in services, and nothing is responsible for keeping the group consistent. Designing aggregates is how a model becomes rich: the root takes ownership of the rules, and the rules stop leaking into the callers.
Common Mistakes
One aggregate per database table. Tables are a storage shape. An aggregate boundary comes from invariants, and a single aggregate commonly spans several tables while a single table may hold a value object that belongs to several different aggregates.
The god aggregate. An aggregate that owns everything related to a concept locks everything related to that concept. Two users editing unrelated parts of the same aggregate collide, and loading the root means loading the world. Vernon's Product that owned all of its backlog items, releases, and sprints is the canonical example.
Leaking members past the root. Returning the mutable line item array from the purchase order lets any caller add, remove, or edit items without the root seeing the change. Holding onto a line item across operations has the same effect one step removed: the caller ends up acting on state the root has since moved past. The root hands members out for the current operation only and exposes methods for every change.
Object references across aggregates. A purchase order that holds a Vendor object has quietly pulled the vendor inside its boundary. The next developer will change the vendor through the purchase order, and the repository will save both in one transaction.
Loading an aggregate to read one field. Aggregates are for changes. A screen that lists purchase order numbers and totals should read from a query or a read model and leave the aggregate alone.
Takeaways
- An aggregate is a cluster of entities and value objects that change together, with one root entity as the only entry point.
- The root owns the invariants of the group. A rule that spans several objects is a method on the root.
- The boundary is a consistency boundary. One transaction changes one aggregate, and everything outside the boundary may lag.
- Draw the boundary from true invariants, keep it small, reference other aggregates by ID, and coordinate across boundaries with domain services and events.
- Repositories, domain services, use cases, and module boundaries are all defined relative to aggregates.
Further Reading
Primary Sources
- Domain-Driven Design: Tackling Complexity in the Heart of Software, Eric Evans (2003), chapter 6 introduces aggregates alongside repositories and factories
- Implementing Domain-Driven Design, Vaughn Vernon (2013), chapter 10 is a full treatment of aggregate design
- Effective Aggregate Design, Vaughn Vernon's three-part essay, hosted by the DDD Community with the PDFs for each part
- Effective Aggregate Design, the same essay on Vernon's own site
Supporting Material
- DDD Aggregate, Martin Fowler's short definition
- How to Use Aggregates for Tactical Design, a talk by Vaughn Vernon
- Domain-Driven Design Reference, Eric Evans's free summary of the pattern definitions