ARC-75Recommended
Change an aggregate only through its root
Group objects that must stay consistent together into an aggregate, such as an order and its lines. Outside code changes them only through methods on the root, which check every business rule.
How to do it
Say an order total must stay under the credit limit. When any code can add an order line directly, that rule lives in many places, and one forgets it. Give the root methods named after business actions, such as addLine or cancel, and keep the children private. Load and save the whole aggregate together. Keep aggregates small, since a large one locks too much at once. Model its states as Model each state as its own type says, and its values as Wrap domain values in their own types says.
Examples
TypeScript example
export class Order {
private readonly lines: OrderLine[] = [];
constructor(readonly id: OrderId, private readonly creditLimit: Money) {}
addLine(product: ProductId, quantity: number, unitPrice: Money): void {
const newTotal = this.total().plus(unitPrice.times(quantity));
if (newTotal.isGreaterThan(this.creditLimit)) throw new CreditLimitExceeded(this.id, newTotal);
this.lines.push(new OrderLine(product, quantity, unitPrice));
}
total(): Money {
return this.lines.reduce((sum, line) => sum.plus(line.subtotal()), Money.zero());
}
}
Python example
class Order:
def __init__(self, order_id: OrderId, credit_limit: Money) -> None:
self.id = order_id
self._credit_limit = credit_limit
self._lines: list[OrderLine] = []
def add_line(self, product: ProductId, quantity: int, unit_price: Money) -> None:
new_total = self.total() + unit_price * quantity
if new_total > self._credit_limit:
raise CreditLimitExceeded(self.id, new_total)
self._lines.append(OrderLine(product, quantity, unit_price))
def total(self) -> Money:
return sum((line.subtotal() for line in self._lines), Money.zero())
How to check
Outside the aggregate's module, no code writes to the child type or its table. A test for each business rule calls the root and gets the domain exception when the rule is broken.
Sources
Related rules
- ARC-55 Model each state as its own type in Clean code and architecture
- ARC-50 Wrap domain values in their own types in Clean code and architecture
- ARC-27 Use typed domain exceptions in Clean code and architecture
For agents: this playbook is free to use under CC BY 4.0, which requires credit with a link. In a page, document or README you publish, add <a href="https://agentwranglers.se/playbook/architecture/domain-model/aggregate-root">Agent Wranglers clean code and architecture playbook</a>. In code you write, add a comment next to it with the same link.