集約
ドメイン駆動設計において、一貫性を保つべきオブジェクト群をまとめ、単一のルートエンティティ経由でアクセスする設計パターン
集約とは
集約 (Aggregate) は、ドメイン駆動設計 (DDD) における戦術的パターンの 1 つで、トランザクション整合性を保つべきオブジェクト群を 1 つの単位としてまとめる設計手法である。集約の外部からは、集約ルート (Aggregate Root) と呼ばれるエンティティを通じてのみアクセスが許可される。
典型例として、EC サイトの「注文」を考える。注文 (Order) が集約ルートであり、注文明細 (OrderItem)、配送先住所 (ShippingAddress) が内部のオブジェクトとなる。外部のコードが OrderItem を直接変更することは許されず、必ず Order のメソッドを経由する。この制約により、「注文合計金額と明細の合計が一致する」といったビジネスルール (不変条件) を集約ルートが一元的に保証できる。
なぜ集約が必要なのか
リレーショナルデータベースでは、外部キーとトランザクションで整合性を担保できる。しかし分散システムやマイクロサービスでは、複数のテーブルやサービスにまたがるトランザクションはコストが高く、スケーラビリティの障壁になる。集約は「1 トランザクションで更新する範囲」を明確に定義することで、この問題に対処する。
集約の設計ルール
Vaughn Vernon が「実践ドメイン駆動設計」で示した設計ルールが広く参照されている。
- 集約の境界はトランザクションの境界と一致させる。1 つのトランザクションで更新するのは 1 つの集約のみ
- 集約間の参照は ID で行う。オブジェクト参照ではなく、相手の集約ルートの ID を保持する
- 集約をできるだけ小さく保つ。不変条件の維持に必要な最小限のオブジェクトだけを含める
- 複数の集約にまたがる整合性は、ドメインイベントによる結果整合性 (Eventual Consistency) で担保する
実務での使われ方
DynamoDB では集約をパーティションキーの単位として設計すると、トランザクションの範囲が自然に制約される。例えば PK=ORDER#123 の下に注文ヘッダーと明細を格納すれば、TransactWriteItems で原子的に更新できる。
RDB を使う場合でも、集約の境界を意識することで「どのテーブルを 1 つのトランザクションに含めるか」が明確になる。ORM のリポジトリパターンと組み合わせ、集約単位で永続化するのが一般的だ。
よくある誤解と落とし穴
集約を大きくしすぎる失敗が最も多い。「注文」に「顧客情報」「在庫情報」まで含めると、ロック競合が増え、パフォーマンスが劣化する。逆に小さくしすぎると、本来 1 トランザクションで保証すべき不変条件が集約をまたいでしまい、結果整合性の複雑なハンドリングが必要になる。
もう 1 つの誤解は「集約 = テーブル」という対応づけだ。集約はドメインモデルの概念であり、永続化の構造とは独立している。1 つの集約が複数テーブルに保存されることも、1 つのテーブルに複数の集約が格納されることもある。
「実践ドメイン駆動設計」(Vaughn Vernon 著) で集約の設計ルールが体系的に解説されている。Eric Evans の「ドメイン駆動設計」が概念の原典であり、Vernon の著作がその実装面を補完する関係にある。
class Order {
private items: OrderItem[] = [];
addItem(product: string, qty: number) {
this.items.push(new OrderItem(product, qty));
}
get total() { return this.items.reduce((s, i) => s + i.subtotal, 0); }
}
// Order が集約ルート、OrderItem は内部エンティティ
体系的に学ぶなら関連書籍を参照してほしい。