Aggregate
Aggregate
"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." — Evans, Domain-Driven Design, 2003
Intent
An Aggregate is a cluster of domain objects — Entities and Value Objects — that are treated as a single unit for the purposes of data changes and consistency enforcement. The boundary of an Aggregate defines what changes together atomically; no invariant-crossing mutation happens across the boundary within a single transaction.
The Aggregate Root is always an Entity. All external access to objects inside the Aggregate must pass through the root. External objects may hold a reference to the root, but never to internal objects directly. This gate-keeping allows the root to enforce all business invariants over the entire cluster.
The Aggregate is the primary consistency boundary in Domain-Driven Design. When you save an Aggregate, you save the entire cluster. When you load an Aggregate, you load the entire cluster. This is why designing aggregates small is a first-order concern — an oversized Aggregate causes transaction contention under load.
When NOT to Use
- The "aggregate" groups entities that do not share true transactional invariants — this leads to oversized aggregates and transaction contention. If two things can change independently, they should be separate Aggregates.
- Accessing an Aggregate triggers loading of many unrelated objects; this is a sign the Aggregate boundary is too wide.
- You need to query individual child entities directly — Repository is per Aggregate Root, not per entity. If you find yourself wanting an
OrderLineRepository,OrderLineprobably belongs in its own Aggregate (or the model needs revisiting). - The "aggregate" has more than three or four child entities — Vernon Rule 2 is being violated; redesign around smaller boundaries.
When to Use
- Multiple domain objects share business invariants that must be enforced atomically:
Ordermust always have at least oneOrderLine;OrderLinequantities must never produce a negative total. - You need a clear consistency boundary to reason about what changes together in a single transaction.
- A domain concept has a natural "owner" entity:
OrderownsOrderLine;OrderLineis never accessed independently. - You need to emit Domain Events as a result of state changes — Aggregate Roots are the canonical source of Domain Events.
How It Works
Vernon's Four Rules of Aggregate Design:
- Model true invariants in consistency boundaries — only invariants that must be enforced transactionally belong inside one Aggregate. If two objects do not share a true business invariant, they should not share an Aggregate boundary.
- Design small aggregates — the root entity plus value-typed properties is the default. Niclas Hedhman's observation: 70% of Aggregates can be just a root Entity + Value Objects; 30% have two to three entities at most. Keep aggregates small to reduce lock contention.
- Reference other Aggregates by identity only — hold a foreign key (an ID value object), not an object reference. This prevents accidental cross-aggregate loading and makes the boundary explicit in code.
- Use eventual consistency outside the boundary via Domain Events — if two Aggregates must react to each other, emit a Domain Event from the first and handle it asynchronously in a separate transaction. Never modify two Aggregate Roots in a single transaction.
Domain Event collection (deferred dispatch): Aggregate Roots collect Domain Events during command execution. Events are dispatched after the transaction commits, not inside the domain method. This preserves Vernon Rule 4 and avoids accidental cross-aggregate coupling within a transaction.
Anti-pattern: Anaemic Domain Model Entities and Value Objects are pure data containers (getters/setters only). All behaviour lives in Service classes. Result: no encapsulation, invariants scattered across services, brittle code. ORM influence (JPA entities mapped from DB tables) is a primary driver of this pattern. Warning signs: Domain entities have only getters/setters; all logic is in
XxxServiceclasses; no validation in constructors. The fix is to move behaviour onto the Aggregate Root — ask "who is responsible for this invariant?" and put the method there.
Class Diagram
classDiagram
class AggregateRoot {
-id : AggregateId
-domainEvents : DomainEvent[]
+execute(command)
+addEvent(event : DomainEvent)
+clearEvents() : DomainEvent[]
}
class Entity {
-id : EntityId
+equals(other) bool
}
class ValueObject {
<<immutable>>
+equals(other) bool
}
class DomainEvent {
<<immutable>>
+occurredOn : Date
+aggregateId : AggregateId
}
AggregateRoot *-- Entity : contains
AggregateRoot *-- ValueObject : contains
Entity *-- ValueObject : contains
AggregateRoot ..> DomainEvent : collects
note for AggregateRoot "Consistency boundary:\nall invariants enforced\nwithin a single transaction"
TypeScript Example
// Aggregate Root — TypeScript
// Source: Vernon, Implementing Domain-Driven Design, 2013
interface DomainEvent { readonly occurredOn: Date; }
abstract class AggregateRoot {
private _domainEvents: DomainEvent[] = [];
protected addDomainEvent(event: DomainEvent) { this._domainEvents.push(event); }
collectDomainEvents(): DomainEvent[] {
const events = [...this._domainEvents];
this._domainEvents = []; // clear after collection (deferred dispatch)
return events;
}
}
class OrderConfirmed implements DomainEvent {
readonly occurredOn = new Date();
constructor(public readonly orderId: string) {}
}
class Order extends AggregateRoot {
private status: 'pending' | 'confirmed' | 'cancelled' = 'pending';
constructor(public readonly orderId: string) { super(); }
confirm(): void {
if (this.status !== 'pending') throw new Error('Order already processed');
this.status = 'confirmed';
this.addDomainEvent(new OrderConfirmed(this.orderId)); // collected, not dispatched yet
}
}
// After transaction commit: const events = order.collectDomainEvents(); dispatcher.publish(events);Java Example
// Aggregate Root — Java
// Source: Vernon, Implementing Domain-Driven Design, 2013
// Spring @DomainEvents + @AfterDomainEventPublication is the framework-managed alternative
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
abstract class AggregateRoot<T> {
private final List<T> domainEvents = new ArrayList<>();
protected void addDomainEvent(T event) { domainEvents.add(event); }
public List<T> collectDomainEvents() {
List<T> events = Collections.unmodifiableList(new ArrayList<>(domainEvents));
domainEvents.clear(); // clear after collection
return events;
}
}
class Order extends AggregateRoot<DomainEvent> {
private String status = "pending";
private final String orderId;
public Order(String orderId) { this.orderId = orderId; }
public void confirm() {
if (!"pending".equals(status)) throw new IllegalStateException("Order already processed");
this.status = "confirmed";
addDomainEvent(new OrderConfirmed(orderId)); // deferred dispatch
}
}Lineage Backward
Composite-Pattern — Lineage chain 7 (Aggregation: Composite → Aggregate → Message Aggregator). The Composite structural pattern is the GoF ancestor for hierarchical composition of objects into tree structures. Aggregate adds domain invariant semantics to the composition boundary: where Composite treats all nodes uniformly, Aggregate enforces that all mutations flow through the root and that the cluster is a single consistency unit.
Lineage Forward
- Domain-Events — Aggregates collect and emit Domain Events as a result of state changes. Vernon Rule 4 mandates that communication between Aggregates happens via events to preserve consistency boundaries.
- Repository — Each Aggregate Root has exactly one Repository that loads and saves the entire Aggregate as a unit. Repository and Aggregate are defined in terms of each other.
- Event-Sourcing-Pattern (Phase 12) — Event Sourcing reconstructs Aggregate state by replaying its Domain Events in sequence. The Aggregate is the unit of reconstitution: all events belong to one Aggregate Root and are replayed in order to restore current state.
Related Concepts
| Pattern | Relationship |
|---|---|
| Value-Object | Building block of Aggregate. Value Objects are owned by the Aggregate Root and are never accessed or persisted independently. |
| Entity | Aggregate Root is always an Entity. Entities inside an Aggregate boundary may exist, but are accessed only through the root. |
| Repository | Persistence boundary per Aggregate Root. One Repository per Aggregate Root — never one per non-root Entity. |
| Domain-Events | Cross-aggregate communication mechanism. Aggregates emit Domain Events; other Aggregates react via event handlers in separate transactions. |
Related Architecture Patterns
- Clean-Architecture — Aggregates live in the Entities ring, the innermost Clean Architecture layer containing enterprise-wide business rules with no dependencies on any outer ring.
Related System Design
- Database-Sharding — DDD aggregates co-located on the same shard by aggregate ID are a natural unit for shard key selection; aggregate boundary and shard boundary align, preventing cross-shard aggregate access
Sources
- Evans, Eric. Domain-Driven Design: Tackling Complexity in the Heart of Software. Addison-Wesley, 2003. p.125.
- Vernon, Vaughn. Implementing Domain-Driven Design. Addison-Wesley, 2013. ch.10.
- Fowler, Martin. bliki/DDD_Aggregate — https://martinfowler.com/bliki/DDD_Aggregate.html