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, OrderLine probably 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: Order must always have at least one OrderLine; OrderLine quantities 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: Order owns OrderLine; OrderLine is 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:

  1. 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.
  2. 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.
  3. 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.
  4. 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 XxxService classes; 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.
PatternRelationship
Value-ObjectBuilding block of Aggregate. Value Objects are owned by the Aggregate Root and are never accessed or persisted independently.
EntityAggregate Root is always an Entity. Entities inside an Aggregate boundary may exist, but are accessed only through the root.
RepositoryPersistence boundary per Aggregate Root. One Repository per Aggregate Root — never one per non-root Entity.
Domain-EventsCross-aggregate communication mechanism. Aggregates emit Domain Events; other Aggregates react via event handlers in separate transactions.
  • 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.
  • 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