Aggregates and Invariants

The transactional boundary inside the domain.

0/2 done

Theory

An aggregate is a cluster of objects treated as a single unit for consistency. It has exactly one aggregate root — the only object the outside world is allowed to touch. The root enforces every invariant.

Rules of thumb:

  • One aggregate per transaction.
  • Reference other aggregates by id only, never by object.
  • Keep them small. Big aggregates = lock contention + slow loads.

Visualization

Order is the aggregate root; OrderLines exist only inside it.

Worked example — Order aggregate as a class diagram

Worked example — an aggregate as a Mermaid class diagram.

Mermaid's classDiagram lets you draw the root + its internals + the by-id reference to another aggregate, all in one block:

classDiagram
  class Order {
    +OrderId id
    +CustomerId customer_id
    +addLine(product_id, qty)
    +confirm()
  }
  class OrderLine {
    +ProductId product_id
    +int qty
    +Money line_total
  }
  Order *-- "1..*" OrderLine : contains
  Order ..> CustomerId : by id only

Reading it:

  • Order is the aggregate root — every operation that mutates an order enters through addLine() / confirm() so invariants stay enforceable.
  • *-- is composition: an OrderLine cannot exist without its Order. No repository for OrderLine.
  • ..> is a dependency on the CustomerId value object — Order references the Customer aggregate by id only, never by holding the actual Customer object.

In the playground below, extend this skeleton with one more invariant of your choosing (e.g. a totalAmount() method on Order).

Reading in progress · 0 of 2 activities done