DDD: Aggregates, Entities, Value Objects — The Tactical Trio
Aggregate = consistency boundary. Entity = identity over time. Value Object = identity by value, always immutable.
When to use
- Enforcing domain invariants that span multiple objects (aggregate)
- Modeling immutable concepts like Money, Address, DateRange (value object)
Tradeoffs
- Large aggregates create write contention (lock the whole thing per transaction)
- Small aggregates need external coordination for cross-aggregate consistency
- Go
- Python
// Value Object — identity by value
type Money struct{ Amount float64; Currency string }
// Entity — identity by ID
type OrderLine struct{ ID string; Product string; Price Money }
// Aggregate Root — enforces invariant
type Order struct {
ID string
Lines []OrderLine
}
func (o *Order) AddLine(line OrderLine) error {
if line.Price.Amount <= 0 {
return errors.New("price must be positive")
}
o.Lines = append(o.Lines, line)
return nil
}
from dataclasses import dataclass
from typing import List
@dataclass(frozen=True) # Value Object: immutable
class Money:
amount: float
currency: str
@dataclass
class OrderLine: # Entity
id: str
product: str
price: Money
@dataclass
class Order: # Aggregate Root
id: str
lines: List[OrderLine]
def add_line(self, line: OrderLine) -> None:
if line.price.amount <= 0:
raise ValueError("price must be positive")
self.lines.append(line)
Gotcha: Only reference other aggregates by their ID — never by object reference. This enforces the boundary.