集約設計(DDD)

英語名 Aggregate Design (DDD)
読み方 アグリゲート デザイン ディーディーディー
難易度
所要時間 1〜2時間
提唱者 Eric Evans
目次

ひとことで言うと
#

ドメインオブジェクトを「一貫性を保つ単位(集約)」にまとめ、トランザクションの境界を明確にする設計手法。データベースの都合ではなくビジネスルールの都合でまとめ方を決めるのがポイント。

押さえておきたい用語
#

押さえておきたい用語
Aggregate(アグリゲート)
関連するオブジェクトを束ねて一貫性の境界を形成するまとまり。1つのトランザクションで整合性を保証する単位になる。
Aggregate Root(アグリゲートルート)
集約の入り口となるルートエンティティ。外部から集約内部のオブジェクトに直接アクセスせず、必ずルート経由で操作する。
Entity(エンティティ)
同一性(ID)で区別されるオブジェクト。注文や顧客のように、属性が変わっても「同じもの」として追跡する必要があるもの。
Value Object(バリューオブジェクト)
属性の値だけで等価性が決まるオブジェクト。金額や住所のように、同じ値なら交換可能である。
Invariant(不変条件)
集約が常に満たすべきビジネスルールを指す。「注文合計が在庫数を超えない」などの制約がこれにあたる。

集約設計の全体像
#

集約設計:ルートエンティティを通じて一貫性を保つ
集約の境界(Aggregate Boundary)Aggregate Root外部からの唯一の入口IDで識別・整合性を管理EntityIDで同一性を識別例:注文明細Value Object値で等価性を判断例:金額・住所Invariant常に満たすビジネス制約例:在庫超過禁止外部サービス / UIRoot経由でのみアクセス
集約設計の進め方フロー
1
不変条件の洗い出し
ビジネスルール上「同時に整合性を保つ必要がある」制約を列挙
2
集約の境界決定
不変条件を共有するオブジェクト群をひとまとまりにする
3
ルートの選定
外部からのアクセス窓口となるエンティティを1つ決める
集約の実装
ルート経由の操作と不変条件チェックをコードに落とす

こんな悩みに効く
#

  • エンティティ間の関係が複雑でどこまでを一つのトランザクションにすべきかわからない
  • マイクロサービスの分割単位が大きすぎたり小さすぎたりして困っている
  • データの不整合が本番環境で頻発している

基本の使い方
#

不変条件を洗い出す

「ビジネスルールとして絶対に壊れてはいけない制約」を列挙する。技術的な制約ではなく、ドメインエキスパートが「これが崩れたらビジネスが成り立たない」と言うものが対象。

  • 「注文の合計金額は明細の合計と一致しなければならない」
  • 「在庫数は0未満にならない」
  • 「予約枠は定員を超えてはならない」
不変条件をもとに集約の境界を決める

同じトランザクション内で守らなければならない不変条件を共有するオブジェクトを1つの集約にまとめる。集約はなるべく小さくする。

  • 注文(Order)と注文明細(OrderLine)は同じ集約
  • 商品(Product)は別の集約(注文時に在庫の参照はするが、同一トランザクションで更新する必要はない)
集約ルートを選定する

集約の中で最も外部との接点が多いエンティティをルートにする。ルートはIDでグローバルに一意であり、集約外部はルートのIDだけを保持する。

  • Order集約のルートはOrderエンティティ
  • OrderLineへは必ずOrder経由でアクセスする
集約間の参照はIDで行う

集約同士はオブジェクト参照ではなくIDで参照する。これにより集約間の結合が疎になり、マイクロサービスへの分割も容易になる。

  • OrderはProduct集約をオブジェクト参照で持たず、productIdで参照
  • 結果整合性(Eventually Consistent)で集約間の整合性を担保する

具体例
#

例1:ECサイトの注文管理システムを設計する

従業員40名のECスタートアップ。注文処理で在庫不整合が月に12件発生し、手動修正に月20時間を費やしていた。

不変条件の整理

不変条件対象オブジェクト
注文合計 = 明細合計Order, OrderLine
明細の数量 > 0OrderLine
割引率は100%を超えないOrder, Discount
在庫数 >= 0Product(別集約)

集約の定義

  • Order集約: Order(ルート)、OrderLine、Discount
  • Product集約: Product(ルート)、StockQuantity

注文確定時にProduct集約の在庫を確認するが、同一トランザクションでは更新しない。在庫引き当てはドメインイベント OrderPlaced を発行して非同期で処理する。

導入後、在庫不整合は月12件から0件に減り、手動修正の20時間がそのまま開発に回せるようになった。

例2:SaaS予約プラットフォームの同時予約制御を改善する

月間予約数15万件の予約管理SaaS。繁忙期にダブルブッキングが発生し、利用店舗から毎月30件以上のクレームが来ていた。

問題の根本原因: 予約スロットと店舗情報を1つの巨大エンティティで管理しており、予約のたびに店舗テーブル全体をロックしていた。ロック競合でタイムアウトが頻発し、リトライ時にダブルブッキングが起きていた。

集約の再設計

  • TimeSlot集約: TimeSlot(ルート)、Capacity。不変条件は「予約数 <= 定員」
  • Reservation集約: Reservation(ルート)、Guest。不変条件は「1予約に1人以上のゲスト」
  • Shop集約: Shop(ルート)、BusinessHours

TimeSlotを独立した小さな集約にしたことで、ロック範囲が「特定の時間枠」だけに縮小。予約処理のスループットが 4倍 に改善し、ダブルブッキングはゼロになった。

例3:物流企業が配送管理の集約を再設計する

1日5,000件の配送を扱う物流企業。配送ステータス更新のAPIレスポンスが平均2秒を超え、ドライバー用アプリの操作性が深刻に悪化していた。

原因を調べると、Shipment集約が配送先、荷物明細、追跡履歴、ドライバー割り当て、請求情報を全部抱え込んでおり、ステータス更新のたびに数百行のデータをロードしていた。

再設計後の集約構成

集約ルート含まれるオブジェクト主な不変条件
ShipmentShipmentPackage, Destination荷物重量合計 <= 車両積載量
TrackingTrackingEventLocation, Timestampイベントは時系列順
BillingInvoiceInvoiceLine請求額 = 明細合計

集約を3つに分離した結果、ステータス更新のAPIレスポンスは 2秒 → 180ms に短縮。ドライバーからの「アプリが重い」という問い合わせもほぼなくなった。

やりがちな失敗パターン
#

  1. 集約を大きくしすぎる — 関連するものをすべて1つの集約に入れると、ロック競合やパフォーマンス劣化が起きる。「同じトランザクションで守るべき不変条件」だけを基準にする
  2. データベースのテーブル構造から集約を決めてしまう — テーブルのJOIN関係と集約の境界は別物。ビジネスルール(不変条件)から決めるのが正しい順序
  3. 集約間をオブジェクト参照でつないでしまう — 集約間はIDで参照する。オブジェクト参照にすると境界が曖昧になり、トランザクションスコープが意図せず広がる
  4. 結果整合性を恐れて全部を即時整合にする — 即時整合が必要なのは集約内だけ。集約間は結果整合性で十分なケースがほとんどで、ドメインイベントを使えば自然に実現できる

まとめ
#

集約設計は 「何を1つのトランザクションで守るか」 をビジネスルールから決める手法。まず不変条件を洗い出し、それを共有するオブジェクト群を小さくまとめるのが基本。集約間はIDで疎結合に保ち、結果整合性を受け入れると、スケーラビリティとデータ整合性を両立できる。最初は小さく始めて、不変条件の追加に応じて境界を調整していくのが現実的な進め方になる。