// Domain-Driven Design · 06
Aggregate
A cluster changed as one unit, through one root, in one transaction.
How it fits together
- Checkout handler uses / calls Order (root) (1 AddLine)
- Order (root) wraps / holds OrderLine (2 adds line)
- Checkout handler fails / error path OrderLine (no direct edit)
- Order (root) wraps / holds Money (Total)
- Order (root) uses / calls Customer (CustomerId only)
- OrderLine uses / calls Product (ProductId only)
Spot it
- Order lines edited directly, total now wrong
- Deadlocks when saving a huge object graph
- Rules checked in services, bypassed elsewhere
It costs
Rules that span aggregates become eventually consistent and need events or a saga.
In code
public sealed class Order(OrderId id, CustomerId customerId){ private const decimal MaxTotal = 10_000m; private readonly List<OrderLine> lines = []; public OrderId Id { get; } = id; public CustomerId CustomerId { get; } = customerId; public OrderStatus Status { get; private set; } = OrderStatus.Draft; public IReadOnlyList<OrderLine> Lines => lines; public decimal Total => lines.Sum(l => l.Quantity * l.UnitPrice); public void AddLine(ProductId productId, int quantity, decimal unitPrice) { if (Status != OrderStatus.Draft) throw new DomainException("A submitted order cannot change."); if (quantity <= 0) throw new DomainException("Quantity must be positive."); if (Total + quantity * unitPrice > MaxTotal) throw new DomainException("Order total cannot exceed 10,000 EUR."); lines.Add(new OrderLine(productId, quantity, unitPrice)); } public void Submit() { if (lines.Count == 0) throw new DomainException("An empty order cannot be submitted."); Status = OrderStatus.Submitted; }}Go deeperThe full explanation and what to ask for · 1 min readExpandCollapse
An aggregate is a cluster of entities and value objects treated as one unit for changes. Outside code holds a reference only to the root, and every change goes through it, so the root can enforce invariants: rules that must hold after every transaction, such as "a submitted order cannot change".
The usual design rules (Vaughn Vernon): protect true invariants inside the boundary, keep aggregates small, reference other aggregates by id, and modify one aggregate per transaction. Rules across aggregates, like reserving stock when an order is submitted, run through domain events and are eventually consistent.
Load and save the whole aggregate, and use optimistic concurrency (a version number) so two users editing the same order cannot silently overwrite each other. A huge aggregate, such as a Customer holding every order ever, causes lock contention and slow loads; that is a sign the boundary is wrong.
Ask for it
- aggregate root
- invariant
- consistency boundary
- reference by id
- eventual consistency
- optimistic concurrency
Make Order an aggregate root: lines change only via AddLine/RemoveLine, reject changes after Submit, cap the total at 10,000 EUR and 50 lines, reference Customer and Product by id, save it in one transaction with a version column, and reserve stock via an OrderSubmitted event.