モジュラーモノリス(Modular Monolith)アーキテクチャ
1. 概要
モジュラーモノリスとは、単一のデプロイ単位(単一プロセス)の中で業務ドメインを明確な境界を持つ独立したモジュールに分割し、モジュール間は公開契約(public API)とイベントによってのみ相互作用するよう強制することで、モノリスの単純な運用とマイクロサービスの疎結合を同時に追求するアーキテクチャスタイルである。
過去10年あまりの間、多くの組織が拡張性とチームの自律性を理由にシステムをマイクロサービスへ分解したが、相当数は期待した効果の代わりに、ネットワーク呼び出しの遅延と失敗、分散トランザクション、サービス間のバージョン互換、観測・追跡の複雑さというコストを抱え込んだ。 特にドメイン境界が十分に安定していない初期の製品を性急に分割した場合、一つの機能変更が複数のサービスにまたがる同時デプロイへ波及する「分散モノリス(distributed monolith)」に帰結する事例が頻繁に見られた。 こうした反省の中で、単一デプロイの単純さは保ちつつ内部構造だけはマイクロサービス並みに厳格にモジュール化しようという接近が再び注目され、これがモジュラーモノリスである。
従来型モノリスの問題は「単一デプロイ」ではなく「内部境界の欠如」にある。 レイヤ(layer)だけで分けられたコードベースは、時間が経つとどの機能がどのデータに触れるのか追跡できない「大きな泥団子(big ball of mud)」へと退化する。 モジュラーモノリスは、デプロイ境界を一つに置く代わりに論理的境界をドメイン単位で立て、モジュール内部の実装を外部から隠蔽して結合度を統制する。 すなわち「デプロイの統合」と「設計の分離」を分けて扱うことが核心的な思考法である。
この接近の必要性は変化の方向で判断する。 ドメイン境界がまだ流動的で、チーム規模が数十名以下であり、運用組織が分散システムを担える成熟度を備えていない段階であれば、モジュラーモノリスが合理的である。 逆に特定のモジュールだけトラフィックが急増して独立した拡張が必須であったり、互いに異なる言語・ランタイム・デプロイ周期が強く要求される状況であれば、そのモジュールだけをサービスとして切り出す選択のほうがよい。 技術士は「モノリスかマイクロサービスか」という二分法ではなく、変更隔離・拡張要求・組織成熟度という軸の上でどの地点に立つべきかを説明できなければならない。
2. 全体構造と核心的な設計原理
モジュラーモノリスは、一つの実行ファイル(またはコンテナ)の中に複数のドメインモジュールが共存しつつ、各モジュールが自らの公開契約の背後に内部を隠す構造である。 外から見れば一つのアプリケーションだが、内から見れば明確に分離された複数の「サービス候補」が集まった形態である。 下図は、注文・決済・配送・在庫モジュールが一つのプロセスの中で公開APIとドメインイベントによってのみ連結される様子を示している。
flowchart TB
subgraph APP["単一デプロイ単位 (一つのプロセス)"]
direction LR
O["注文モジュール\nPublic API + 内部実装の隠蔽"]
P["決済モジュール\nPublic API + 内部実装の隠蔽"]
S["配送モジュール\nPublic API + 内部実装の隠蔽"]
I["在庫モジュール\nPublic API + 内部実装の隠蔽"]
BUS["インプロセスイベントバス"]
O -->|公開API呼び出し| P
O -->|OrderConfirmed イベント| BUS
BUS -->|購読| S
BUS -->|購読| I
end
UI["Web・モバイルクライアント"] --> O
O --> DBO[("注文スキーマ")]
P --> DBP[("決済スキーマ")]
S --> DBS[("配送スキーマ")]
I --> DBI[("在庫スキーマ")]
この構造で最も重要な規則は、モジュールが他のモジュールの内部クラスやテーブルを直接参照できないという点である。
注文モジュールが決済を必要とすれば、決済モジュールの公開API(例: PaymentPort.authorize(...))のみを呼び出し、決済内部の承認ロジックや保存構造は知ることができない。
こうすれば、決済モジュールを全面的に書き換えたり、後に別のサービスとして分離したりしても、公開契約さえ維持すれば注文モジュールは影響を受けない。
二つめの原理はデータ所有権の分離である。 すべてのモジュールが一つのデータベースを共有していても、各モジュールは自らのスキーマ(あるいはテーブル集合)のみ書き込み権限を持ち、他モジュールのテーブルを直接結合しない。 他モジュールのデータが必要であれば、結合の代わりに公開APIを通じて問い合わせるか、イベントで受け取って自らの読み取りモデルに複製する。 この規則が崩れると「DBレベルですべてが連結された」従来型モノリスへ即座に退行するため、モジュラーモノリスの成否を分ける最も敏感な地点である。
三つめの原理は依存方向の統制である。 モジュール間の依存が循環すると分離が不可能になるため、共有カーネル(shared kernel)とドメイン別モジュールを区別し、依存グラフが非循環(acyclic)になるよう設計する。 たとえば共通の識別子・値オブジェクト・イベント定義は薄い共有モジュールに置きつつ、業務規則は各ドメインモジュールのみに置く。
2.1 モジュール境界の識別
モジュール境界をどこに引くかはアーキテクチャではなくドメインの問題である。 ドメイン駆動設計(DDD)の境界づけられたコンテキスト(bounded context)が最も有力な候補であり、同じ用語が互いに異なる意味を持つ地点(たとえば注文における「顧客」とマーケティングにおける「顧客」)がまさにモジュールを分ける線となる。 境界を誤って引くと一つの機能変更が複数のモジュールを同時に触れることになり結合度がむしろ大きくなるため、変更が共に起こる凝集した概念を一つのモジュールにまとめるのが原則である。
実務では初期には境界を緩く取り、変更ログやコミットがどのモジュールに集中するかを観察しながら境界を再調整する進化的接近が安全である。 一度に完璧な境界を設計しようとする試みは、ドメイン理解が不足する初期にかえって誤った境界を固着させる危険が大きい。
2.2 境界強制(enforcement)の手段
境界は文書や口頭の約束ではなく、道具によって強制されるときにのみ維持される。
Java陣営ではSpring ModulithやArchUnitによって「AモジュールがBモジュールのinternalパッケージを参照すればビルド失敗」といった規則をテストとして固定する。
.NETではプロジェクト参照をモジュール単位で分割し内部型をinternalに制限し、Java 9以上ではモジュールシステム(JPMS)やパッケージアクセス制御で公開範囲を狭める。
こうした自動検証がなければ、忙しい日程の中で開発者たちが手軽な近道(直接参照)を選ぶようになり、境界は数か月以内に消滅する。
3. モジュール間の相互作用と内部構造
モジュール間通信は大きく同期呼び出しと非同期イベントに分かれ、それぞれの選択が結合度と一貫性に直接影響する。 下図は、注文生成1件が決済・在庫・配送モジュールへ伝播する流れを同期・非同期の混合で表現したものである。
sequenceDiagram
participant C as クライアント
participant O as 注文モジュール
participant P as 決済モジュール
participant B as イベントバス
participant I as 在庫モジュール
participant S as 配送モジュール
C->>O: 注文生成リクエスト
O->>P: 公開API呼び出し (決済承認)
P-->>O: 承認結果の返却
O->>O: 確定注文の保存
O->>B: OrderConfirmed イベント発行
B->>I: 在庫差し引き処理
B->>S: 配送準備処理
O-->>C: 注文番号の応答
同期呼び出しは即座に結果が必要な場合に使う。 注文確定の前に決済承認の可否を必ず知らなければならないため、注文モジュールは決済モジュールの公開APIをインプロセスで呼び出し、結果を待つ。 インプロセス呼び出しはネットワークを経由しないため遅延が事実上なく、同じトランザクション境界の中で処理できるため分散トランザクションの複雑さを避けられる点がマイクロサービス比で決定的な長所である。
非同期イベントは結果を即座に待つ必要がなく、発信モジュールが受信モジュールの存在を知らなくてよい場合に使う。
注文確定後の在庫差し引きと配送準備は注文モジュールの関心事ではないため、注文モジュールはOrderConfirmedイベントのみを発行し、誰がそれを購読するかは知らない。
こうすれば、後に「ポイント積立」モジュールを追加しても注文モジュールのコードを変える必要なくイベントを購読するモジュールを加えるだけでよいため、開放閉鎖の原則がモジュール水準で実現される。
ここで注意すべき設計の落とし穴は、「同じプロセス」という便利さに頼ってイベントを単純なメソッド呼び出しで代替することである。 インプロセスイベントであっても発行購読の契約を明示的に置いてこそ、後にそのモジュールを別のサービスとして分離するときにイベントをメッセージブローカ(例: Kafka)へ変える作業が局所的になる。 逆にイベントを乱用してすべての同期フローまで非同期で処理すると、単一プロセスであえて最終的一貫性を甘受する逆説が生じるため、均衡が必要である。
3.1 トランザクションと一貫性
モジュラーモノリスの大きな利点は、複数のモジュールの変更を一つのローカルトランザクションにまとめられる点である。 注文保存と決済記録を同じトランザクションでコミットすれば部分失敗によるデータ不整合を根本的に防げるため、マイクロサービスでSaga・補償トランザクションによって難しく達成していた一貫性を簡単に得られる。 ただしイベント基盤の非同期処理部分は依然として最終的一貫性であるため、イベント発行と状態変更の原子性のためにトランザクショナルアウトボックス(transactional outbox)に類似したパターンをモジュール内部に適用するのが安全である。
3.2 内部パッケージ構造
各モジュールは内部的にapi(公開契約)、domain(業務規則)、infrastructure(保存・外部連動)、internal(隠蔽実装)に分けるのが一般的である。
公開されるのはただapiパッケージのインターフェースとイベント・DTOのみであり、残りはモジュールの外から参照不可にして塞ぐ。
この構造はヘキサゴナルアーキテクチャ(ポートとアダプタ)をモジュール単位で反復適用したものと見なすことができ、一つのモジュールの中でも業務規則が技術の細部に汚染されないよう守る。
4. マイクロサービス・従来型モノリスとの比較
三つのアーキテクチャはしばしば優劣の関係として誤解されるが、実際には互いに異なる制約を最適化した選択肢である。 核心的な差は「デプロイ単位の数」と「境界強制の手段」から生じ、その差が運用コストと変更柔軟性のトレードオフへとつながる。
| 区分 | 従来型モノリス | モジュラーモノリス | マイクロサービス |
|---|---|---|---|
| デプロイ単位 | 1個 | 1個 | N個 |
| モジュール境界 | 曖昧(レイヤ中心) | 明確(ドメイン・道具で強制) | 物理的分離 |
| モジュール間通信 | 直接呼び出し・共有テーブル | 公開API・インプロセスイベント | ネットワーク(REST・gRPC・メッセージ) |
| トランザクション | 単一ローカル | 単一ローカル | 分散(Saga・補償) |
| 独立拡張 | 不可 | 不可(全体拡張のみ) | モジュール別に可能 |
| 運用複雑度 | 低い | 低い | 高い |
| チーム自律性 | 低い | 中間 | 高い |
表が示すように、モジュラーモノリスは運用複雑度を低く保ちつつ境界を明確にするという点で従来型モノリスと決定的に異なる。 一方でマイクロサービスが提供する「モジュール別の独立拡張」と「言語・ランタイムの異質性」は放棄する。 したがって、トラフィックが特定のモジュールに集中して独立拡張が核心要求となる瞬間こそが、モジュラーモノリスからマイクロサービスへの転換信号である。
実際の事例はこのトレードオフを鮮明に示す。 電子商取引プラットフォームShopifyは、大規模なRuby on Railsコードベースをマイクロサービスへ分割する代わりに、境界が強制されたモジュラーモノリスへ再構成し、単一デプロイの利点を守りつつ数千名規模の開発組織が並行して作業できるようにした。 ストリーミング事業者Netflix系列ではないAmazon Prime Videoチームは2023年、映像品質監視パイプラインを分散マイクロサービスから単一アプリケーションへ統合し、インフラコストを約90%削減したと公開したが、これは過度な分散がかえって非効率を生みうることを示した代表的事例である。 顧客データプラットフォームSegmentもまた、数百個へと増えたマイクロサービスの運用負担に耐えられず再びモノリスへ統合した経験を共有しており、これらの転換の共通の教訓は「分離はただではなく、境界が安定した後に遅らせても遅くない」というものである。
5. 深化: 転換戦略と最新動向
モジュラーモノリスが特に強力な地点は、マイクロサービスへ向かう「中間駅」として機能しうる点である。 マーチン・ファウラーが提示した「モノリス優先(Monolith First)」原則は、ドメイン境界が不確実な初期に性急にサービスを分割すると誤った境界を物理的に固着させるため、まず一つのデプロイ単位の中で境界を練り、その境界が安定した後に必要なモジュールのみをサービスとして切り出せと勧告する。 モジュラーモノリスはこの戦略の自然な土台となる。モジュール間通信がすでに公開APIとイベントへ抽象化されているため、分離時点でインプロセス呼び出しをリモート呼び出しへ、インプロセスイベントをメッセージブローカへ変える作業がモジュール境界に限られる。
道具のエコシステムもこの接近を支えるよう成熟した。
Spring陣営のSpring Modulithはモジュール境界を明示し、違反をテストで検証し、モジュール間のイベントと文書化を支援する。
ArchUnitはパッケージ依存規則をコードで断定してCIで境界違反を遮断し、.NETのNDependやアーキテクチャテストライブラリも類似の役割を果たす。
このように「境界を破ればビルドが壊れる」自動化が普遍化するにつれ、モジュラーモノリスはもはや規律のみに依存する脆弱なパターンではなく、道具で支えられる実践可能なアーキテクチャとなった。
最近のクラウド環境では単一デプロイであっても水平拡張(同一イメージを複数インスタンスへ)と無停止デプロイが普遍化し、「モノリスは拡張できない」という通念が相当部分緩和された。 サーバーレス・コンテナオーケストレーションの上でモジュラーモノリスを運用すれば、アプリケーション全体を複製してトラフィックを分散する方式で相当な規模まで担える。 結局、独立デプロイ・独立拡張が本当に必要な少数のモジュールのみを選別的に分離する「ハイブリッド」形態が現実的な支配構造として定着しつつある。
6. 考慮事項および示唆点
第一に、境界強制の自動化が成否を分ける。
モジュラーモノリスは開発者の善意ではなく、ビルド・テスト道具で境界を守るときにのみ維持される。
ArchUnit・Spring ModulithのようなアーキテクチャテストをCIパイプラインの必須ゲートに置き、境界違反がマージされる前に遮断する体系をアーキテクチャ設計と同時に整えなければならない。
第二に、データ所有権を設計段階で釘づけにしなければならない。 最も多い失敗は「性能のために」他モジュールのテーブルを直接結合することから始まる。 モジュール別のスキーマ分離と交差結合の禁止を規約として明文化し、他モジュールのデータが必要であれば公開API問い合わせやイベント基盤の読み取りモデル複製で解決するよう案内してこそ、長期的に分離可能性が保存される。
第三に、転換は境界が安定した後へ遅らせるのが合理的である。 初期からマイクロサービスへ行く決定は、ドメイン理解が最も不足する時点で最も高価な構造を選ぶ逆説である。 モジュラーモノリスで始めて変更パターンとトラフィック特性を観測し、独立拡張・独立デプロイ・技術の異質性が実際に要求されるモジュールのみを選別的に分離する戦略が、コストと危険を共に下げる。
第四に、組織構造とアーキテクチャを共に設計しなければならない。 コンウェイの法則により、システム構造は組織の疎通構造に似る。 モジュラーモノリスでモジュール境界とチーム責任境界を一致させればコード所有権が明確になり変更衝突が減るが、一つのリポジトリ・デプロイを共有するためデプロイ調整とコードレビュープロセスをチーム間で合意しておいてこそ統合の利点が維持される。
第五に、観測可能性とモジュール単位の指標を共に設計する。 単一プロセスであっても、モジュール別の処理時間・エラー率・イベント滞留を分けて測定すれば、どのモジュールがボトルネックでどのモジュールが先に分離対象かをデータで判断でき、転換の意思決定の根拠となる。
参考資料
- Martin Fowler, MonolithFirst
- Spring, Spring Modulith Reference Documentation
- Amazon Prime Video Tech, Scaling up the Prime Video audio/video monitoring service and reducing costs
一言まとめ: モジュラーモノリスは単一デプロイの単純さを保ちつつドメインモジュールを公開APIとイベントによってのみ連結し境界を道具で強制することで、マイクロサービスの疎結合を低コストで得て、必要なモジュールのみを後にサービスとして分離できるようにするアーキテクチャである。