モノリスかマイクロサービスか:変更・データ・運用からサービス境界を決める
サービス数を増やしても、変更の待ち時間が減るとは限りません。注文・在庫・通知の例で、モジュラーモノリスとサービス分割を比較し、トランザクション、契約テスト、運用責任まで一緒に設計します。
最初に描くのはサービス図より、変更が待たされる場所
受注アプリの納期表示を変えるたびに、注文・在庫・通知の担当者が集まり、全員のリリースを待つとします。アプリを3つに分けても、同じ会議と一斉リリースが続くなら、調整の問題は残っています。DORAは、他チームを細かく待たずに変更・テスト・デプロイできることを、疎結合なチームと設計の重要な性質として扱っています。
- 最近の変更を追い、誰の確認・どのテスト環境・どのリリースを待ったかを記録する。
- 同時に変更されたコードとDBの表を調べる。単に同じフォルダーだからではなく、同じ業務ルールで変わるかを聞く。
- 遅延の原因が設計の結合か、承認手順か、担当者不足かを分ける。分割で解決する範囲を明確にする。
注文と在庫の境界は、守りたい制約から決める
「確保できた在庫の数だけ注文を確定する」という条件を、今は同じDBトランザクションで守っているとします。注文サービスと在庫サービスを分けて別DBにすると、そのトランザクションをそのまま使うことはできません。予約・期限切れ・失敗時の取り消しを含む状態遷移など、新しい整合性の設計が要ります。
| 業務のまとまり | 正本と更新責任 | 境界を越える約束 |
|---|---|---|
| 注文 | 注文状態は注文モジュールが更新 | 確定・取消の意味、重複した要求の扱い |
| 在庫 | 引当可能数と予約状態は在庫モジュールが更新 | 予約の成否、失効の検知、取消と再試行の扱い |
| 通知 | 送信状態は通知モジュールが更新 | イベントの重複、再送、送信不能の確認 |
共有DBでも、他モジュールの表を自由に更新しないという責任の境界は作れます。注文と在庫を同一トランザクションで扱う必要があるなら、公開インターフェースを通して同じトランザクションに参加させる設計も候補です。論理的なモジュール境界と、別プロセス・別DBにする境界を区別します。
モジュラーモノリスとサービス分割を同じ条件で比較する
モジュラーモノリスは、公開する入口と依存方向を決めたモジュールを、1つのアプリとして配置する構成です。Martin FowlerのMonolith Firstは、サービス運用の追加負担と初期の境界の不確実さを論じています。これは必ずモノリスから始めるという法則ではなく、チームの経験や既知の境界も含めて判断する材料です。
| 観点 | モジュラーモノリス | サービス分割 |
|---|---|---|
| 変更と公開 | 内部を分けても、配置する単位は共通 | 互換性を保てれば個別に配置できる |
| データ整合性 | 同じDBのトランザクションを使える場合がある | 別DB間の失敗・再送・補償を設計する |
| 容量 | アプリ全体を増やすのが基本。重い処理の分離も検討 | 負荷の違う部分を個別に増やしやすい |
| 障害の影響 | プロセスや資源を共有する影響がある | 分離の機会があるが、同期依存で障害が伝播しうる |
| 運用 | 配置・監視・調査する単位が少ない | 各サービスに監視、配布、認証、復旧の仕組みが要る |
今回なら、まず注文・在庫・通知をモジュールとして分ける案が考えられます。通知の負荷や外部サービス障害が注文受付に影響しているなら、通知を非同期で分離する価値を検証します。注文と在庫は、整合性と運用の代償に見合う理由が確認できるまで、同じ配置単位に置く選択もできます。
境界を、コードの検査とデータの責任で守る
ディレクトリを分けただけでは、注文から通知の内部クラスを呼び、通知の表を書き換える依存が増えます。公開インターフェースだけを利用するルールと、禁止する依存を決めます。ArchUnitは、Javaコードのパッケージ依存や循環などを検査するための仕組みを提供しています。
orders -> inventory.public
orders -> notifications.public
禁止: orders -> inventory.internal
禁止: orders -> notifications.internal
禁止: notifications -> orders.internal
更新責任: 各モジュールの表はそのモジュールの処理から更新
検証: 依存検査 + 業務ルールのテスト + 重要経路の結合テスト静的な依存検査だけでは、生のSQLによる別モジュールの表の更新や、動的な呼び出しまで保証できません。DBアクセスの実装、必要に応じた権限、レビュー、結合テストも合わせます。既存の違反を一度に直せない場合は件数と箇所を把握し、新しい違反が増えたら検査で止めます。
通知を取り出すなら、通信と復旧の約束を先に決める
注文と送信予定を同じDBトランザクションで保存 → 配送処理が送信予定を読み、イベントを送る → 通知サービスがイベントIDで重複を扱う → 外部への送信結果を記録し、不明な結果を照会・確認
注文保存後にHTTPで通知するだけだと、その間の停止で通知が抜けます。送信予定を注文と同じトランザクションで保存するOutboxなどを検討します。配送を再試行すれば重複しうるため、通知側でも重複を扱います。外部へのメール送信の直後に落ちた場合など、ローカルの処理済み記録だけでは二重送信を防げない境界もあります。
- 契約:イベントの版、必須項目、順序、同じ注文の再送時の意味を決める。契約テストと主要経路のE2Eを使い分ける。
- 失敗:タイムアウト、再試行の上限、処理不能なイベントの保管先と再処理の担当を決める。
- 運用:受付から通知までを追えるID、滞留時間の監視、必要な認可、担当の対応時間と代替要員を用意する。
分割の成功は、サービス数より変更と運用の結果で測る
分割前後で、変更が出るまでの待ち時間、同時リリースが必要な頻度、障害の影響範囲、復旧に要した手間を比べます。サービスが増えても毎回まとめてテスト・公開しているなら、契約やデータの結合を再確認します。モジュラーモノリスを続ける場合も、共有する配置と資源がどこまで許容できるかを定期的に見直します。
設計レビューでは「通知の滞留を注文受付から分離したい。まず更新責任と非同期の契約を固め、変更待ちと障害時の影響で効果を確かめる」と説明できます。構成の名前に加えて、分割する目的と支払う代償が伝わります。