DDD 02 戦略的設計(境界づけられたコンテキスト・サブドメイン・コンテキストマップ)
更新日:
← DDD_01_考え方(ドメイン・モデル・ユビキタス言語) / 入口 → DDD / 次 → DDD_03_戦術的設計①(エンティティ・値オブジェクト・ドメインサービス)
戦略的設計 = システム全体をどう分け、どこに力を入れるかを決める「大きな地図」の話。エヴァンス自身も「戦術(部品)より戦略の方が重要」と後に強調している。
1. 境界づけられたコンテキスト(Bounded Context)
ある言葉・モデルが、ひとつの一貫した意味を持つ範囲。
同じ「商品」でも、部署によって関心が違う:
| コンテキスト | 「商品」の意味 | 持つ情報 |
|---|---|---|
| カタログ | 売り場に並べる物 | 名前・説明・画像・カテゴリ |
| 価格 | 値段をつける対象 | 定価・セール価格・税区分 |
| 在庫 | 倉庫にある物 | SKU・数量・保管場所 |
| 配送 | 運ぶ荷物 | 重さ・サイズ・配送区分 |
✗ これを全部持った1つの巨大な Product クラスを作る → 誰が何を変えても全員に影響する
○ コンテキストごとに別の Product モデルを持ち、共通なのは ID だけ にする
[カタログ] [在庫] [配送]
Product StockItem Parcel
id ─────────────── productId ────────── productId
name quantity weight
description location sizeポイント:
- コンテキストの内側では言葉の意味を1つに固定(ユビキタス言語はコンテキストごと)
- 境界の目安:言葉の意味が変わるところ、担当チーム・部署が変わるところ、変更のタイミングが違うところ
- 実装上の境界は、モジュール(フォルダ)・パッケージ・サービス・DB スキーマなど。最初はモノリス(1アプリ)の中のフォルダ分けで十分(モジュラーモノリス)
- マイクロサービスに分けるなら、コンテキストが分割の良い目安になる。ただし**「1コンテキスト=1マイクロサービス」は必須ではない**
2. サブドメインと重要度
業務全体(ドメイン)を領域に分け、どこに設計の力をかけるかを決める。
| 種類 | 意味 | 例(美容院予約サービス) | 方針 |
|---|---|---|---|
| コアドメイン | 他社との差別化・競争力の源泉 | 空き枠の最適な提案、指名・メニュー組合せの予約ロジック | 最も優秀な人と時間を投入。DDD を本格的に適用 |
| 支援サブドメイン | 事業に必要だが差別化しない、既製品もない | 店舗スタッフのシフト登録 | シンプルに作る。CRUD でもよい |
| 汎用サブドメイン | どの会社にも共通 | 認証、決済、メール送信 | 既製品・SaaS を使う(認証 は自作しない) |
- サブドメイン = 問題側の分け方(業務を見て分ける)
- 境界づけられたコンテキスト = 解決側の分け方(ソフトウェアをどう分けるか)
- 理想は1対1だが、現実には一致しないこともある
3. コンテキストマップ
コンテキスト同士がどう連携しているかを描いた図。チームの力関係も表す。
| パターン | 関係 | 使いどころ |
|---|---|---|
| 共有カーネル | 2つのコンテキストでモデルの一部を共有 | 密に協力できる2チーム。変更時は両者合意が必要 |
| 顧客/供給者 | 上流(供給者)が下流(顧客)の要望を聞いて提供 | 社内チーム間で協力関係がある |
| 順応者(Conformist) | 下流が上流のモデルをそのまま受け入れる | 上流が変えてくれない(大手の API など)が、そのまま使っても困らない |
| 腐敗防止層(ACL) | 下流が翻訳層を作り、上流のモデルを自分の言葉に変換 | 外部 API・レガシーシステムの都合を持ち込みたくない |
| 公開ホストサービス(OHS)+ 公表された言語(PL) | 上流が誰でも使える公開 API とデータ形式を用意 | 多くの利用者がいる API(REST・GraphQL・イベント) |
| 別々の道 | 連携しない | 統合コストに見合わない |
[決済SaaS] (上流・変えられない)
│
▼ 腐敗防止層で翻訳
[注文コンテキスト] ──顧客/供給者── [配送コンテキスト]
│
▼ ドメインイベント「注文が確定した」
[ポイントコンテキスト]腐敗防止層(Anti-Corruption Layer)の例
外部決済サービスのレスポンスをそのまま使わず、自分の言葉に翻訳する。
// 外部の都合: { st: "succeeded" | "requires_action" | ..., amt: 1200, cur: "jpy" }
// 自分のモデル: PaymentResult
export class PaymentGatewayAdapter implements PaymentGateway {
async charge(order: Order): Promise<PaymentResult> {
const res = await externalClient.createCharge({ amt: order.total.amount, cur: 'jpy' });
// 外部の言葉 → 自分の言葉に翻訳。外部の型はこのファイルの外に出さない
return res.st === 'succeeded' ? PaymentResult.paid(res.id) : PaymentResult.failed(res.st);
}
}→ 決済サービスを乗り換えても、変えるのはこのファイルだけ。
4. 進め方の目安
- 業務の出来事を並べる(イベントストーミング)
- 言葉の意味が変わる場所・担当が変わる場所に線を引く → コンテキスト候補
- 各領域をコア/支援/汎用に分類 → 力の入れどころを決める
- コンテキスト間の関係(上流・下流、翻訳の有無)を図にする
- 最初は1つのアプリの中でフォルダを分ける。必要になってからサービス分割
5. 初学者向けまとめ
- 「全部入りの巨大モデル」を作らない。同じ言葉でも場所によって別モデルでよい
- 力をかけるのはコアドメインだけ。認証・決済などは既製品
- 外部サービスの型・用語は翻訳層で止めて、自分のコードに染み込ませない