shredbx logo
shredbx shredbx shredbx shredbx Personal
  • Home
  • Lab
  • Portfolio
  • Experience
  • Services
  • Profile
  • Contact
AClaude
  • Home
  • Lab
  • Portfolio
  • Experience
  • Services
  • Profile
  • Contact
Andrei Solovev
Knowledge
Search knowledge... ⌘K
Knowledge · Patterns · Design Patterns

Aggregate

DDD Aggregate — consistency boundary with single root Entity controlling all access

Andrei Solovev

Tags

dddaggregatemodelingentitiesconsistency

Overview

Purpose

DDD Aggregate — consistency boundary with single root Entity controlling all access

Context

Domain models with clusters of closely related Entities and Value Objects that share invariants — rules that must hold true across the cluster as a whole, not just within individual objects. The system requires transactional consistency guarantees for these clusters. Multiple clients or threads may attempt concurrent modifications to objects within the same cluster. The persistence layer supports atomic transactions at some granularity (row, document, or explicit transaction scope) that can be aligned with the cluster boundary.

Problem

How to maintain invariants that span multiple domain objects when those
objects can be modified concurrently, and how to define clear ownership
and lifecycle boundaries for clusters of related objects?

Forces:
(1) Invariants span multiple objects: "order total <= credit limit" requires
    examining all OrderLines together, not individually
(2) Concurrent modifications to related objects can violate cross-object
    invariants if not coordinated within a transaction
(3) Unrestricted object graphs create a web of references where any object
    can reach any other, making transactional boundaries undefined
(4) Deleting an object requires knowing what other objects should be deleted
    with it (cascade) vs what should merely lose a reference (nullify)
(5) Large transactional scopes reduce concurrency and increase contention;
    small scopes risk invariant violations — the boundary must be precisely
    sized to the invariant scope
(6) External systems need stable reference points — they cannot reference
    internal objects that may be restructured or deleted without notice

Solution

Define an Aggregate as a cluster of associated Entities and Value Objects
with a clearly defined boundary and a single Entity designated as the
Aggregate Root. Enforce three rules:

1. The Root is the only member accessible from outside the Aggregate.
   External objects hold references to the Root only, never to internal
   members. The Root may hand out transient references to internals, but
   the caller must not hold them beyond the immediate operation.

2. All invariants spanning objects within the boundary are enforced by the
   Root on every state-changing operation. The Root is the invariant
   guardian — no modification to any internal object bypasses the Root.

3. Persistence operates at the Aggregate level. A Repository exists only
   for the Root. The entire Aggregate is loaded, modified, and saved as
   a unit within a single transaction. Delete cascades to all internals.

The boundary is sized to match the invariant scope: everything that must
be consistent together belongs in one Aggregate, and nothing more.

Consequences

Benefits:
- Clear consistency boundaries: invariants are enforced atomically within
  a well-defined scope, eliminating cross-object consistency bugs
- Concurrency control at the right granularity: optimistic locking on the
  Root protects the entire cluster without global locks
- Lifecycle clarity: cascade-delete from Root to internals is unambiguous —
  no orphaned objects, no dangling references to deleted internals
- Simplified persistence: one Repository per Aggregate Root, one transaction
  per Aggregate operation, one unit of loading/saving
- Reference discipline: external references only to Roots prevents the
  tangled object graphs that make systems incomprehensible

Liabilities:
- Boundary design is difficult: too large and concurrency suffers (lock
  contention); too small and invariants cannot be enforced atomically
- Cross-aggregate invariants require eventual consistency — not all business
  rules can be enforced within a single transaction, requiring domain events
  and compensating actions
- Loading the entire Aggregate for every operation may be expensive if the
  Aggregate contains many internal objects (the "large aggregate" problem)
- Refactoring boundaries is costly: changing what is inside vs outside an
  Aggregate affects Repositories, transactions, and reference patterns
- Developers must resist the temptation to enlarge Aggregates for
  convenience, which degrades performance and concurrency

Collaborations

Application services invoke operations on AggregateRoot, which is the sole entry point for all modifications. AggregateRoot coordinates with its InternalEntities and ValueObjects to execute the operation, then validates all cross-object invariants before confirming the change. Repository loads the complete Aggregate (Root + all internals) from persistence and saves it atomically after modifications. External objects reference only the AggregateRoot by identity — when they need information about internals, they ask the Root, which may return transient references or copies. Cross-aggregate communication happens through domain events or by referencing other Aggregate Roots by identity (never by direct object reference to avoid tight coupling and transactional entanglement).

Structure

Participants: - AggregateRoot: Single entry-point Entity, invariant guardian, externally referenceable - InternalEntity: Locally-identified Entity within boundary, not externally accessible - ValueObject: Immutable descriptive component within boundary - Repository: Per-Root persistence, loads/saves entire Aggregate atomically

Relationships: - AggregateRoot contains InternalEntity (containment, cascade-delete) - AggregateRoot contains ValueObject (composition, value semantics) - InternalEntity contains ValueObject (composition, value semantics) - Repository manages AggregateRoot (persistence lifecycle) - External objects reference AggregateRoot by identity only (association)

Boundary rules: - Inside boundary: Entities + Value Objects with shared invariants - Outside boundary: other Aggregates, referenced by Root identity only - Cascade-delete: deleting Root deletes ALL internals - Transaction scope: one Aggregate = one transaction

Implementation

Design guidelines:

1. Size Aggregates to invariant scope, not convenience
   - Ask: "what objects MUST be consistent together in a single transaction?"
   - Include only those objects. Everything else is a separate Aggregate.
   - Prefer small Aggregates — Vernon's "design small aggregates" principle
   - Large Aggregates cause lock contention and loading overhead

2. Reference other Aggregates by identity, not object reference
   - Go: use ProductID type, not *Product pointer
   - This prevents loading cascades and transactional entanglement
   - Enables Aggregates to be in different services or databases

3. One Repository per Aggregate Root
   - Repository interface in domain layer, implementation in infrastructure
   - Load/save the entire Aggregate atomically
   - Never create Repositories for internal Entities

4. Enforce invariants in the Root, not in services
   - Application services orchestrate; the Root validates
   - Root's methods should be named for domain operations, not CRUD
   - order.addLineItem() not order.setLineItems()

5. Handle cross-aggregate invariants with eventual consistency
   - Use domain events: OrderPlaced triggers InventoryReservation
   - Accept that some invariants span Aggregates and cannot be atomic
   - Design compensating actions for failures (Saga pattern)

6. Use optimistic concurrency on the Root
   - Version number or timestamp on the Root Entity
   - Concurrent modifications to the same Aggregate are detected and retried
   - This is sufficient because all modifications go through the Root

7. In SBX .framework, the three-map PD architecture directly implements
   Aggregate semantics:
   - `entities` map = internal Entities (cascade-delete = aggregate boundary)
   - `attributes` map = Value Objects (value-bound = aggregate-internal values)
   - `references` map = cross-aggregate associations (independent lifecycle)
   This is not a coincidence — the entity.yml protocol was designed with
   Aggregate boundary semantics as the structural foundation.

Example

Consider an online ordering system where an Order contains multiple OrderLines, each referencing a Product with a price. A business rule states that the order total must never exceed the customer's credit limit. A developer models Order and OrderLine as independent Entities, each with its own Repository. Two concurrent requests arrive: one adds a new OrderLine, another modifies an existing OrderLine's quantity. Each request loads its own copy of the relevant OrderLine, makes its change, and saves — neither checks the invariant against the full set of lines. The order total now exceeds the credit limit because no single transaction encompassed all the OrderLines together. The system has no defined boundary for "what must be consistent together." Without an Aggregate, there is no way to enforce cross-object invariants atomically.

Resolution

Applying the Aggregate pattern to the ordering system: Order becomes the Aggregate Root. OrderLines become Internal Entities within the Order Aggregate boundary. Product remains a separate Aggregate, referenced by ProductId only. All operations on OrderLines go through Order: order.addLineItem(), order.removeLineItem(), order.updateQuantity(). The credit limit invariant is checked in every Order method that changes the total. A single OrderRepository loads and saves the complete Order with all its OrderLines atomically. Concurrent requests to modify the same Order are serialized through optimistic locking on the Order Root. The invariant violation from the original example is now impossible — both requests go through the Root, which checks the total against the credit limit before confirming either change.

Known Uses

  • Order + OrderLines in virtually every e-commerce system (Amazon, Shopify, Magento) — Order as Root, Lines as internals, Product as separate Aggregate referenced by ID
  • Account + Transactions in banking systems (Evans DDD reference) — Account as Root enforcing balance invariants across all Transactions
  • ShoppingCart + CartItems in retail platforms — Cart as Root, Items as internals with quantity/price invariants, cascade-delete on cart abandonment
  • Axon Framework (Java) — @Aggregate annotation maps directly to the DDD Aggregate pattern with command handling on the Root and event sourcing
  • MongoDB document model — each document is naturally an Aggregate; embedded sub-documents are internals, DBRefs are cross-aggregate references

See Also

  • entity-pattern: The Aggregate Root is always an Entity. Internal members may also be Entities (with local identity) or Value Objects. The Entity pattern provides the identity and lifecycle semantics that the Root needs.
  • value-object: Aggregates compose Value Objects as descriptive components within their boundary. Value Objects share the Aggregate's transactional scope and are deleted with the Root.
  • typed-map-composition: SBX entity.yml's three-map PD architecture (entities, attributes, references) implements Aggregate semantics structurally. The entities map with cascade-delete IS the aggregate boundary in YAML form.
design structural
shredbx logo shredbx shredbx shredbx shredbx Andrei Solovev

Solution Architect & Lead Software Engineer

ExperiencePortfolioResearch & ExperimentsEducationCertificationSkills
GitHub ↗LinkedIn ↗Email ↗
AVAILABLE FOR NEW PROJECTS
// MY LATEST BEATS
Hobby & Interests

Lab

  • The Lab
  • Framework
  • Components
  • Packages
  • Games
  • Process (SDLC)
  • Knowledge
  • Blog

Andrei

  • Portfolio
  • Experience
  • Services
  • Profile
  • Contact
  • Lifestyle

Team

  • Team
  • Andrei
  • Claude

Legal

  • Privacy
  • Terms
  • Cookies
© 2026 shredbx.com. All rights reserved. — Andrei Solovev |