Concept · Architecture & domain modeling
Aggregates
A cluster of domain objects changed as one unit through a single root, which enforces its invariants — the consistency boundary of a DDD model.
An aggregate is a group of entities and value objects that is treated as a single unit for changes, with one entity as its root. It exists to protect invariants — business rules that must always hold — such as "an order's total equals the sum of its lines" or "a placed order can't be edited".
The rules
- Outside code references only the root. It may hold the root and call its methods, but never modify an inner object (an order line) directly.
- The root enforces every invariant in its methods, so no code path can leave the aggregate invalid.
- One transaction modifies one aggregate. Inside the aggregate, consistency is immediate; between aggregates, it's eventual — coordinated through domain events.
- Aggregates refer to each other by ID, not by object reference: an
Orderholds aCustomerID, not a*Customer. - One repository per aggregate root, loading and saving it whole.
type Order struct { // root
id OrderID
lines []OrderLine // unexported: only Order's methods change it
status Status
}
func (o *Order) AddLine(item ItemID, price Money, qty int) error {
if o.status != StatusDraft {
return ErrNotEditable
}
if len(o.lines) >= 50 {
return ErrTooManyLines
}
o.lines = append(o.lines, OrderLine{item: item, price: price, qty: qty})
return nil
}The contrast is an anemic model: structs with public fields and all rules in service functions, where any code can write order.Status = "PLACED" and skip the checks.
Sizing aggregates
Keep them small. Include only what must be consistent in the same transaction. A Restaurant aggregate containing every order ever placed would lock all of a restaurant's orders on each change and load huge amounts of data. Instead, Order and Restaurant are separate aggregates, and a rule spanning them ("can't order from a closed restaurant") is checked by the use case before calling the order, or handled eventually through events.
Ask of each invariant: does it really need to be true at every instant, or within seconds? Many rules that look immediate — loyalty points, stock counts across warehouses, analytics — tolerate eventual consistency, which keeps aggregates small and fast.
Aggregates are built from value objects and entities, and publish changes through events, often via a transactional outbox. See the domain-driven design lesson.