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. 業務の出来事を並べる(イベントストーミング)
  2. 言葉の意味が変わる場所・担当が変わる場所に線を引く → コンテキスト候補
  3. 各領域をコア/支援/汎用に分類 → 力の入れどころを決める
  4. コンテキスト間の関係(上流・下流、翻訳の有無)を図にする
  5. 最初は1つのアプリの中でフォルダを分ける。必要になってからサービス分割

5. 初学者向けまとめ

  • 「全部入りの巨大モデル」を作らない。同じ言葉でも場所によって別モデルでよい
  • 力をかけるのはコアドメインだけ。認証・決済などは既製品
  • 外部サービスの型・用語は翻訳層で止めて、自分のコードに染み込ませない