集約

ドメイン駆動設計において、一貫性を保つべきオブジェクト群をまとめ、単一のルートエンティティ経由でアクセスする設計パターン

DDD設計パターン

集約とは

集約 (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 は内部エンティティ

体系的に学ぶなら関連書籍を参照してほしい。

関連用語