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.

3 minaggregate rootDDDinvariantsanemic domain 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

  1. 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.
  2. The root enforces every invariant in its methods, so no code path can leave the aggregate invalid.
  3. One transaction modifies one aggregate. Inside the aggregate, consistency is immediate; between aggregates, it's eventual — coordinated through domain events.
  4. Aggregates refer to each other by ID, not by object reference: an Order holds a CustomerID, not a *Customer.
  5. One repository per aggregate root, loading and saving it whole.
go
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.

Learn it in depth