境界をどこに引くのか
一言でいうと
境界は名詞からではなく、言葉から生まれます。同じ単語を2つのチームが異なる意味で使っているなら、そこが境界です。
なぜ必要なのか
「注文サービス、商品サービス、会員サービスに分けましょう。」会議室で最もよく出る一文であり、最もよく失敗する設計です。この方式は、データベースのテーブル名をそのままサービス名に格上げしたものにすぎません。結果は予測できます。注文を1つ作るのに、商品サービスと会員サービスをそれぞれ呼び出さなければならず、3つのサービスが1つの画面のために常に一緒に動きます。デプロイの独立性は生まれません。
ドメイン駆動設計が提案する視点は異なります。境界は、データの種類ではなく、そのデータについて話す人たちの言葉から生まれます。
どう動くのか
同じ「商品」という単語を見てみましょう。営業チームにとって、商品は価格、割引、表示の有無です。物流チームにとって、商品は容積、重量、保管温度です。精算チームにとって、商品は手数料率と税コードです。この3つを1つの商品テーブルにまとめると、物流の要件でカラムを追加するたびに、営業チームのコードが再デプロイされます。
この地点が、境界づけられたコンテキストです。各コンテキストは自分だけの商品モデルを持ち、コンテキスト間は識別子(例: SKU)だけで結びつけます。コンテキストの境界を越えるときにモデルを翻訳するのが、前に見た腐敗防止層です。
実務で境界の候補を見つける方法は、3つあります。1つ目は、同じ単語の意味が分かれる地点。2つ目は、トランザクションが実際に必要な範囲です。1つのトランザクションの中になければならないデータは、1つのサービスの中にあるべきです。3つ目は、変更の頻度です。毎週変わる部分と、1年に一度変わる部分は、別のサービスである可能性が高いでしょう。
現場での姿
境界が間違っているというシグナルは、コードではなく会議で先に現れます。1つの機能をデプロイするのに、常に2つのチームのスケジュール調整が必要なら、境界が間違っています。そして、訂正する方法は、サービスをさらに分割することではなく、たいてい2つのサービスを元に統合することです。統合する決定は、分割する決定よりはるかに少なくしか下されませんが、はるかに頻繁に正解です。
もう1つ。マイクロサービスで「共通ライブラリ」は、静かな結合です。共通のDTOを1つのリポジトリに置くと、そのリポジトリのバージョンを上げるときに、すべてのサービスが一緒に動かなければなりません。契約は、コードの共有ではなく、スキーマのドキュメント(OpenAPI、protobuf)で共有するほうが安全です。
境界を見つける実際の手順
「言葉から見つける」という話は正しいのですが、実行するのは難しいものです。現場で使う手順があります。
イベントストーミング: ドメインの専門家と開発者を1つの部屋に集めて、壁に「起きたこと」を過去形 で貼ります。注文が受け付けられた、決済が承認された、在庫が引き当てられた、配送が開始された。 次に、これらの出来事を時間順に並べて、誰がその出来事に反応するのかを示します。 反応が集まる塊が、境界の候補です。
結合度を数える: 候補の境界を引いておいて、実際のコードで呼び出しの回数を数えます。
| 指標 | 良い値 | 悪い場合に意味すること |
|---|---|---|
| 1つの画面を描画するのに必要なサービス数 | 1–2 | 3以上なら、境界が間違っている |
| 1つの機能のデプロイに関わるチーム数 | 1 | 2以上なら、境界がチームをまたいでいる |
| サービス間の同期呼び出しの深さ | 1–2 | 3以上なら、障害が連鎖する |
モノリスから始めます: 最初から分けると、たいてい間違えます。ドメインを知らない状態で 引いた線だからです。1つのリポジトリの中でモジュールとして境界を引き(モジュラーモノリス)、 その境界が6か月間揺らがなければ、そのときにサービスとして切り出します。間違って引いたモジュールの境界は 半日で移せますが、間違って分けたサービスは数か月かかります。
分けてはいけないとき
境界をうまく引くのと同じくらい、分けないと決めることも設計です。次の条件では、 サービスとして切り出すのは、ほとんど常に損です。
- チームが1つです。マイクロサービスの利点は、チームの独立したデプロイです。チームが1つなら、 その利点はなく、運用の負担だけが残ります。
- 強いトランザクションが必要です。在庫の引き当てと注文の作成が、必ず一緒に成功するか 一緒に失敗しなければならないなら、2つを分けた瞬間に、サーガ(saga)と補償トランザクションを自分で 作らなければなりません。その複雑さは、得られるものより大きくなります。
- ドメインをまだ知りません。新しい製品の最初の6か月は、境界が毎週変わります。
運用コストも併せて数えます。サービスを1つ増やすと、リポジトリ・CI・デプロイのパイプライン・ダッシュボード・ アラート・オンコールのドキュメントが、それぞれ1つずつ増えます。サービス5つのシステムは、コードが5 倍ではなく、運用の面積が5倍です。
データが境界を越えるとき
境界を引くと、すぐに「では結合(join)はどうするのか」という問題が来ます。注文一覧に 商品名を表示しなければならないのに、商品は別のサービスにあります。答えは3つあり、 それぞれコストが異なります。
- 呼び出して取得する。最も単純ですが、商品サービスが落ちると、注文一覧も落ちます。 1つの画面に、2つのサービスの可用性を掛け合わせることになります(99.9% × 99.9% = 99.8%)。
- 必要な分だけ複製しておく。注文に商品名を注文時点の値として埋め込んでおきます。 実は、これがドメインの観点でも正解です。商品名が後で変わっても、過去の注文書の 名前はそのままでなければなりません。
- 参照専用のビューを作る。イベントを購読して、読み取り専用のテーブルを維持します(CQRS)。 最も柔軟ですが、結果整合性と再構築の手順を引き受けなければなりません。
2つ目が正解の場合が、思ったより多くあります。「参照」として見るのか、「その時点の事実」 として見るのかを先に決めれば、答えが出ます。
次のクイズで確認すること
このモジュールは概念だけを扱います。すぐ続くクイズで境界の判断基準を点検し、次のモジュールで、そのように分けたサービスが実際に通信するとき、何を支払うのかを手で確認します。