Stackbook Logo
data-consistencyestablished · low operational burden

Aggregate

Also known as: ddd-aggregate, aggregate-root

Intent

Group related entities and value objects into a consistency boundary with a single root that enforces invariants.

Problem

When any object can modify any other, business invariants are violated by scattered code paths, and transactions grow to span arbitrary object graphs. Concurrency conflicts and locking become unpredictable.

Forces

  • •Invariants must hold after every committed change
  • •Transactions should be small to avoid contention
  • •Object graphs tend to grow without explicit boundaries
  • •Cross-aggregate rules still need to be satisfied eventually

Solution

Overview

An aggregate is the unit of consistency in domain-driven design. Everything inside is consistent after each transaction; everything outside is consistent eventually. The aggregate root is the only entry point.

How it works

Take an Order with line items. A rule says the total must never exceed the customer credit limit. Line items are only meaningful inside an order, so Order is the root and OrderLine is internal.

Order (root)
  |-- OrderLine
  |-- OrderLine
  customerId  --> reference by ID to Customer aggregate
class Order:
    def add_line(self, sku, qty, price):
        if self.total() + qty * price > self.credit_limit:
            raise CreditExceeded()
        self._lines.append(OrderLine(sku, qty, price))

The repository loads and saves the whole Order, often with a version column for optimistic concurrency. If shipping must react to an order being placed, the Order emits a domain event and a separate transaction updates the Shipment aggregate.

Pitfalls

  • Making aggregates large because the data is related; relation does not imply a shared invariant.
  • Modifying two aggregates in one transaction, which erodes the boundary.
  • Exposing internal collections so callers mutate them directly.
  • Designing aggregates from database tables instead of from invariants.
  • Ignoring the version check, which allows lost updates.

Key takeaways

  • Draw boundaries around invariants, not around relationships.
  • One transaction modifies one aggregate.
  • Reference other aggregates by ID.
  • Prefer small aggregates and eventual consistency between them.

✓ When to Use

  • •Domains with real invariants spanning several objects
  • •Systems with concurrent writers needing clear locking scope
  • •When modelling with domain-driven design

✗ When Not to Use

  • •Pure CRUD data with no invariants
  • •Reporting or read-heavy models
  • •When the domain is not yet understood well enough to find boundaries

Pros

  • +Invariants enforced in one place
  • +Natural transaction and concurrency boundary
  • +Smaller units scale and shard well

Cons

  • −Wrong boundaries are costly to change
  • −Cross-aggregate queries and rules need extra work
  • −Loading whole aggregates can be heavy

Cost Profile

Infrastructure

None — modelling approach

Operational

Low — affects data access patterns

Cognitive

High — boundary design needs domain insight

Failure Modes

  • Oversized aggregates causing frequent optimistic lock conflicts

  • Undersized aggregates that cannot enforce invariants atomically

  • Direct references between aggregates that bypass the root

Real-World Examples

Alternatives

    Related Patterns

      Competency Domains

      data staterequirements constraints