d9e5668cec
Fixes and improvements from exhaustive audit: Consistency fixes in SKILL.md: - 'Pipe & Filter' → 'Pipe and Filter' (one stray '&' found and corrected) - 'Singleton for factory instance' → clarified to 'register factory as singleton-scoped via DI container' (less misleading wording) - Documentation Format section updated with note that SKILL.md itself is the authoritative source for related-pattern combinations Coverage fix — Related Patterns sections: - Added '## Related Patterns' to ALL 94 pattern files (was 0/94) - Each section lists 3–6 related patterns with relationship descriptions - Covers: why they're related, when to prefer one vs the other, and which are often confused SOLID principles → Creational → Structural → Behavioral → Architectural → Concurrency → Functional → Resilience → Data Access → Messaging → Testing → Error Handling → Microservice — all 13 categories covered Code verification: - Python: 0 failures (all 85 testable blocks pass) - Go: 0 failures (all 76 testable blocks pass) - JavaScript: 0 failures (all 78 testable blocks pass) - All 239 code blocks verified correct after edits Final skill state: - 108 files, 36,524 lines across 13 reference categories - 94/94 pattern files have Related Patterns sections - 2,815-line SKILL.md with 67 decision trees, 23 scenarios, 0 broken references, 0 naming inconsistencies
9.1 KiB
9.1 KiB
Aggregate Pattern
Problem
In a complex domain, multiple related objects must be kept consistent. An Order with LineItems must maintain invariants like "total equals sum of line items" and "can't remove the last item." Without clear boundaries, any code can modify any object, leading to inconsistent state and hard-to-find bugs.
Solution
Define an aggregate — a cluster of related domain objects treated as a single unit for data changes. The aggregate has:
- Aggregate root — The single entry point. All external access goes through the root.
- Invariants — Business rules the aggregate enforces internally.
- Transactional boundary — The entire aggregate is loaded and saved as one unit.
External code never reaches inside the aggregate to modify child objects directly.
When to Use
- Domain-Driven Design with complex domain models
- When multiple objects must maintain consistency invariants
- When you need clear transactional boundaries
- Order + LineItems, Account + Transactions, Cart + Items
When to Avoid
- Simple CRUD without business invariants
- Read-heavy systems where aggregates add unnecessary indirection
- When objects don't have meaningful consistency rules
- Reporting/analytics queries (use read models instead)
Pseudocode
class Order (Aggregate Root):
items: list[LineItem]
function add_item(product, quantity, price):
enforce invariants
items.add(LineItem(product, quantity, price))
recalculate_total()
function remove_item(product):
enforce: items.length > 1
items.remove_by(product)
recalculate_total()
# External code cannot directly modify items
Python Implementation
from dataclasses import dataclass, field
from typing import Optional
@dataclass(frozen=True)
class Money:
amount: float
currency: str = "USD"
def add(self, other: "Money") -> "Money":
return Money(round(self.amount + other.amount, 2), self.currency)
def multiply(self, n: int) -> "Money":
return Money(round(self.amount * n, 2), self.currency)
def __str__(self):
return f"${self.amount:.2f}"
@dataclass
class LineItem:
product: str
quantity: int
unit_price: Money
@property
def subtotal(self) -> Money:
return self.unit_price.multiply(self.quantity)
def __repr__(self):
return f"LineItem({self.product!r}, qty={self.quantity}, subtotal={self.subtotal})"
class OrderError(Exception):
pass
class Order:
"""Aggregate root — all modifications go through this class."""
def __init__(self, order_id: str, customer: str):
self.id = order_id
self.customer = customer
self._items: list[LineItem] = []
self._status = "draft"
def add_item(self, product: str, quantity: int, unit_price: Money) -> None:
if self._status != "draft":
raise OrderError(f"Cannot modify order in '{self._status}' status")
if quantity <= 0:
raise OrderError("Quantity must be positive")
# Check if product already exists
for item in self._items:
if item.product == product:
item.quantity += quantity
return
self._items.append(LineItem(product, quantity, unit_price))
def remove_item(self, product: str) -> None:
if self._status != "draft":
raise OrderError(f"Cannot modify order in '{self._status}' status")
matching = [i for i in self._items if i.product == product]
if not matching:
raise OrderError(f"Product '{product}' not in order")
self._items = [i for i in self._items if i.product != product]
def place(self) -> None:
if not self._items:
raise OrderError("Cannot place empty order")
if self._status != "draft":
raise OrderError(f"Cannot place order in '{self._status}' status")
self._status = "placed"
@property
def total(self) -> Money:
result = Money(0)
for item in self._items:
result = result.add(item.subtotal)
return result
@property
def item_count(self) -> int:
return len(self._items)
@property
def status(self) -> str:
return self._status
@property
def items(self) -> tuple:
"""Read-only view of items."""
return tuple(self._items)
def __repr__(self):
return (f"Order(id={self.id!r}, customer={self.customer!r}, "
f"items={self.item_count}, total={self.total}, status={self._status!r})")
# --- Demo ---
print("=== Build order through aggregate root ===")
order = Order("ORD-001", "Alice")
order.add_item("Widget", 2, Money(9.99))
order.add_item("Gadget", 1, Money(24.99))
print(f" {order}")
print(f" Items: {order.items}")
print()
print("=== Aggregate enforces invariants ===")
# Add more of existing product (aggregate merges)
order.add_item("Widget", 3, Money(9.99))
print(f" After adding 3 more Widgets: qty is now {order.items[0].quantity}")
print(f" Total: {order.total}")
print()
print("=== Place order ===")
order.place()
print(f" Status: {order.status}")
# Try to modify placed order
try:
order.add_item("Doohickey", 1, Money(5.00))
except OrderError as e:
print(f" Modification blocked: {e}")
print()
print("=== Invariant: can't place empty order ===")
empty = Order("ORD-002", "Bob")
try:
empty.place()
except OrderError as e:
print(f" Blocked: {e}")
print()
print("=== Invariant: quantity must be positive ===")
draft = Order("ORD-003", "Charlie")
try:
draft.add_item("Thing", -1, Money(10.00))
except OrderError as e:
print(f" Blocked: {e}")
Output:
=== Build order through aggregate root ===
Order(id='ORD-001', customer='Alice', items=2, total=$44.97, status='draft')
Items: (LineItem('Widget', qty=2, subtotal=$19.98), LineItem('Gadget', qty=1, subtotal=$24.99))
=== Aggregate enforces invariants ===
After adding 3 more Widgets: qty is now 5
Total: $74.94
=== Place order ===
Status: placed
Modification blocked: Cannot modify order in 'placed' status
=== Invariant: can't place empty order ===
Blocked: Cannot place empty order
=== Invariant: quantity must be positive ===
Blocked: Quantity must be positive
JavaScript Implementation
class Money {
constructor(amount, currency = "USD") {
this.amount = Math.round(amount * 100) / 100;
this.currency = currency;
Object.freeze(this);
}
add(other) { return new Money(this.amount + other.amount, this.currency); }
multiply(n) { return new Money(this.amount * n, this.currency); }
toString() { return `$${this.amount.toFixed(2)}`; }
}
class LineItem {
constructor(product, quantity, unitPrice) {
this.product = product;
this.quantity = quantity;
this.unitPrice = unitPrice;
}
get subtotal() { return this.unitPrice.multiply(this.quantity); }
toString() { return `LineItem(${this.product}, qty=${this.quantity}, subtotal=${this.subtotal})`; }
}
class OrderError extends Error {}
class Order {
#items = [];
#status = "draft";
constructor(id, customer) {
this.id = id;
this.customer = customer;
}
addItem(product, quantity, unitPrice) {
if (this.#status !== "draft") throw new OrderError(`Cannot modify '${this.#status}' order`);
if (quantity <= 0) throw new OrderError("Quantity must be positive");
const existing = this.#items.find((i) => i.product === product);
if (existing) { existing.quantity += quantity; return; }
this.#items.push(new LineItem(product, quantity, unitPrice));
}
place() {
if (this.#items.length === 0) throw new OrderError("Cannot place empty order");
if (this.#status !== "draft") throw new OrderError(`Cannot place '${this.#status}' order`);
this.#status = "placed";
}
get total() {
return this.#items.reduce((sum, i) => sum.add(i.subtotal), new Money(0));
}
get status() { return this.#status; }
get items() { return [...this.#items]; }
toString() {
return `Order(id=${this.id}, items=${this.#items.length}, total=${this.total}, status=${this.#status})`;
}
}
// --- Demo ---
console.log("=== Build order ===");
const order = new Order("ORD-001", "Alice");
order.addItem("Widget", 2, new Money(9.99));
order.addItem("Gadget", 1, new Money(24.99));
console.log(` ${order}`);
order.addItem("Widget", 3, new Money(9.99));
console.log(` After adding 3 more Widgets: total=${order.total}`);
order.place();
console.log(` Status: ${order.status}`);
try { order.addItem("X", 1, new Money(1)); }
catch (e) { console.log(` Blocked: ${e.message}`); }
console.log("\n=== Empty order ===");
try { new Order("ORD-002", "Bob").place(); }
catch (e) { console.log(` Blocked: ${e.message}`); }
Related Patterns
- Repository — repositories load and store entire aggregates as atomic units; no partial aggregate loading.
- Value Object — aggregates are composed of entities and value objects; value objects carry the immutable attributes.
- Guard Clause — aggregates enforce invariants using guard clauses inside their mutating methods.
- Domain Events (Observer) — aggregates raise domain events (observer pattern) when significant state changes occur.