Everyone who reads the blue book comes away understanding what an aggregate is: a cluster of domain objects treated as a unit for data changes. The hard part is drawing the boundary.
The Default Mistake
Most teams make aggregates too large. An Order that contains Customer, Product, Inventory, and ShippingAddress as nested entities isn't an aggregate — it's the entire database in object form.
// Too large — this aggregate locks everything
final class Order
{
private Customer $customer;
private Collection $products; // full Product entities
private Inventory $inventory; // why is this here?
private ShippingAddress $address;
}
When two users modify the same aggregate concurrently, one of them loses. The larger the aggregate, the more conflicts you create.
The Rule: Protect True Invariants
An aggregate boundary exists to protect a business invariant — a rule that must always be true within a single transaction.
Ask yourself: "What data must be consistent at all times, not eventually?"
For an order, the answer is probably: line items and their quantities must match the order total. That's it. The customer, the product catalog, the shipping address — those are separate aggregates referenced by ID.
final class Order
{
private OrderId $id;
private CustomerId $customerId; // reference, not entity
private Collection $lineItems; // OrderLineItem value objects
private Money $total;
public function addItem(ProductId $productId, int $quantity, Money $unitPrice): void
{
$this->lineItems->add(new OrderLineItem($productId, $quantity, $unitPrice));
$this->recalculateTotal();
}
}
Small Aggregates, Eventual Consistency
If two things don't need to be consistent in the same transaction, they belong in separate aggregates. Coordinate them with domain events.
// When an order is placed, update inventory eventually
final class OrderPlaced
{
public function __construct(
public readonly OrderId $orderId,
public readonly array $lineItems,
) {
}
}
The inventory adjustment happens in a separate transaction, triggered by the event. If it fails, you handle it — but you don't lock the entire order process while inventory catches up.
The Four Rules
Vaughn Vernon's four rules of aggregate design are worth memorizing:
- Protect business invariants inside aggregate boundaries
- Design small aggregates
- Reference other aggregates by identity only
- Update other aggregates using eventual consistency
Practical Heuristic
If you're struggling with boundaries, start with one entity per aggregate. Then ask: "What else must change atomically with this entity?" Only add what's truly required. Everything else is a separate aggregate.
The Takeaway
Small aggregates with clear invariants are easier to reason about, easier to test, and dramatically reduce contention in concurrent systems. When in doubt, make it smaller.