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.
Also known as: ddd-aggregate, aggregate-root
Group related entities and value objects into a consistency boundary with a single root that enforces invariants.
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.
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.
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.
None — modelling approach
Low — affects data access patterns
High — boundary design needs domain insight
Oversized aggregates causing frequent optimistic lock conflicts
Undersized aggregates that cannot enforce invariants atomically
Direct references between aggregates that bypass the root