Agent Wranglers

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

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.