モジュラモノリスとは?モジュール境界の設計とDB分割・依存検証の実装

モジュラモノリスは、デプロイ単位を1つに保ったまま、コードの内部を業務領域ごとのモジュールへ分ける設計です。「分割したいがサービス間通信の運用コストは負いたくない」という中間解として選ばれます。ただし境界を口約束で運用すると、半年後には元の一枚岩に戻ります。ここでは境界の引き方、依存をテストで検証する具体的な方法、データベースの分け方、そしてマイクロサービスへ切り出す順序を扱います。マイクロサービスとの比較や選び方の判断軸はマイクロサービスとは?モノリスとの違い・メリット・デメリットと選び方、モジュラモノリスまで整理にまとめてあります。

まとめ|モジュラモノリスで押さえる要点

  • モジュラモノリスは1プロセス・1デプロイ単位のまま、内部の境界を強制する設計です。プロセスを分けない点がマイクロサービスとの決定的な違いです。
  • 境界は規約ではなくツールで守ります。Javaなら Spring Modulith 2.1.0、Ruby on Railsなら Packwerk 3.3.0 が違反をテストで落とせます。
  • モジュールの切り方は業務領域です。「controller層・service層」で切ると、どの機能を触っても全層に手が入る元の構造に戻ります。
  • データベースはスキーマ単位で所有権を分け、モジュールをまたぐ参照はIDで持ちます。JOINを許した時点で境界は消えます。
  • 単一デプロイのままなので、トランザクションは1つのDBで完結します。この一貫性を捨てずに済む点が、分散化との最大の差です。

モジュラモノリスとは|単一デプロイのまま境界を強制する設計

従来のモノリスとの違いは「境界が検証されるか」

従来のモノリスにもモジュール分割の意図はありました。違いは、その境界が守られているかを機械が判定できるかどうかです。ディレクトリを分けただけの構造では、隣のモジュールのクラスを直接importしても何も起きません。モジュラモノリスでは、この直接参照がビルドやテストで失敗します。

境界を強制する仕組みが入ると、コードの見通しの担保がレビュー担当者の記憶に依存しなくなります。新しく入った開発者が「この機能はどこまで触ってよいか」を、ツールの出力で確認できる状態になります。

プロセスを分けないことで残る利点

マイクロサービスへ進むと、モジュール間の呼び出しはネットワーク越しになります。障害の切り分け、リトライ、結果整合性の設計、監視基盤の整備がすべて必要です。モジュラモノリスはこれらを負いません。モジュール間の呼び出しは関数呼び出しのままで、データベースのトランザクションも1つで完結します。

その代わり、スケールはアプリケーション全体を複製する形になります。特定の機能だけをスケールさせたい要件が明確にあるなら、そこはマイクロサービスの領分です。

モジュール境界の引き方

レイヤーではなく業務領域で切る

境界の単位は、注文・在庫・請求といった業務領域です。ドメイン駆動設計の用語では境界づけられたコンテキストにあたります(用語の整理はドメイン駆動設計(DDD)とは?意味・基本用語・実装と採用判断をわかりやすく解説を参照)。

src/main/java/com/example/
├─ Application.java
├─ order/              ← モジュール(業務領域)
│   ├─ OrderService.java        公開してよい入口
│   └─ internal/                 他モジュールから触らせない実装
├─ inventory/
└─ billing/

各モジュールは「入口となる少数のクラス」と「外から触らせない実装」に分けます。Spring Modulithはパッケージ直下のpublicクラスだけを公開扱いとし、配下のサブパッケージは既定で非公開になります。公開範囲を明示したい場合は @NamedInterface を使います。

依存の検証をテストで自動化する

境界の宣言だけでは守られません。CIで落ちる状態にして初めて機能します。Javaでは Spring Modulith 2.1.0 の検証を1つのテストとして書きます。

class ModularityTests {

  @Test
  void verifiesModularStructure() {
    var modules = ApplicationModules.of(Application.class).verify();  // 違反があれば失敗

    new Documenter(modules)
        .writeModulesAsPlantUml();      // モジュール構成図を生成
  }
}

モジュール単位の結合テストには @ApplicationModuleTest を付けます。公式リポジトリのREADMEにあるサンプルは @ApplicationModuleTests と複数形で書かれていますが、実際に存在するのは単数形の ApplicationModuleTest です。コピーして貼るとコンパイルが通りません。

Ruby on RailsではShopify製の Packwerk 3.3.0 が同じ役割を担います。パッケージごとに package.yml を置き、enforce_dependencies: true と依存先の一覧を宣言します。

# components/order/package.yml
enforce_dependencies: true
dependencies:
  - components/shared      # ここに書いていないパッケージの定数参照は違反

# 検出と構成検証
$ bin/packwerk check
$ bin/packwerk validate

既存の大規模アプリへ後から入れる場合、初回の check は数千件の違反を出します。すべてを直してから始めるのは非現実的なので、現状を既知の違反として記録し、新規の違反だけをCIで落とす運用から入ります。増やさない状態を作ってから、件数を減らしにいきます。

データベースの分割|スキーマ単位の所有権

コードだけ分けてもテーブルを共有していれば、境界はSQLから破られます。テーブルごとに所有モジュールを決め、他モジュールからの直接参照を禁止します。物理的に分けなくても、PostgreSQLのスキーマやMySQLのデータベース単位で名前空間を割り当てれば運用できます。

order_schema.orders          ← orderモジュールが所有
inventory_schema.stocks      ← inventoryモジュールが所有

-- 禁止:他モジュールのテーブルへ直接JOINする
SELECT * FROM order_schema.orders o
  JOIN inventory_schema.stocks s ON s.item_id = o.item_id;

-- 許可:IDだけを保持し、必要な情報は相手モジュールの入口から取得する

外部キー制約も、モジュールをまたぐ場合は張りません。制約が残っている限り、将来サービスとして切り出すときにスキーマ変更が連鎖します。整合性はアプリケーション側で担保し、境界をまたぐ参照はIDのみという規則を徹底します。

ただし、単一デプロイである以上、DB接続は1つのままで構いません。ここを最初から物理分割すると、分散トランザクションの問題だけを先に抱え込むことになります。

モジュール間の連携|直接呼び出しとイベント

同期の呼び出しは、相手モジュールの公開クラスを経由します。実装クラスに触れない限り、これは境界違反ではありません。一方、「注文が完了したら在庫を減らす」のような通知は、イベントで疎結合にする方が依存の向きを増やさずに済みます。

@Component
class InventoryManagement {

  @ApplicationModuleListener      // @Async + @Transactional(REQUIRES_NEW) + @TransactionalEventListener
  void on(OrderCompleted event) {
    // 在庫を減らす
  }
}

Spring Modulithはイベント発行時に、配送先リスナーごとの行をEvent Publication Registryへ書き込み、処理が完了した時点で消し込みます。アプリケーションが途中で落ちても未完了の記録が残るため、再送が可能です。未完了分は IncompleteEventPublications から扱え、2.0以降は resubmitIncompletePublications(ResubmissionOptions) による再送に対応しています。状態は処理中がPROCESSING、失敗がFAILED、再送済みがRESUBMITTEDです。

この「イベントの取りこぼしを検出できる」仕組みは、モジュール間の非同期連携を入れるなら必須の装備です。素の @EventListener だけで組むと、落ちた瞬間のイベントは痕跡なく消えます。

マイクロサービスへ切り出す順序

モジュラモノリスは、そのまま運用し続けてもよい構成です。切り出すのは、特定モジュールに独立したスケール要件や独立したリリース速度が必要になったときだけで十分です。

切り出す場合の順序は、境界が数か月安定してからです。モジュール間の呼び出しがまだ頻繁に変わる段階でサービス化すると、変更のたびに2つのデプロイを同期させる作業が発生します。手順としては、対象モジュールのDBスキーマを先に分離し、同期呼び出しをイベントへ置き換え、最後にプロセスを分けます。逆順で進めると、分散トランザクションの問題に最初にぶつかります。

モジュラモノリスを採用すべきでない場面

単一チーム・十数ファイル規模のアプリケーションには不要です。 境界の維持にはツール導入とCI設定、違反時の設計判断というコストが常時かかります。1人が全体を把握できる規模では、そのコストが利益を上回ります。

すでにマイクロサービスで運用が回っていて、監視も自動化も整っているなら、統合し直す理由もありません。逆行は移行コストだけを払う結果になりがちです。

また、モジュール分割の合意形成ができない組織では導入しても機能しません。境界の位置は業務の理解そのものなので、開発チームだけで決めた境界は数か月で例外だらけになります。ツールは合意を守らせる道具であって、合意を作る道具ではありません。

よくある質問

モジュラモノリスとは何ですか?

デプロイ単位を1つに保ったまま、内部を業務領域ごとのモジュールに分け、その境界をツールで強制する設計です。プロセスを分けないため、モジュール間の呼び出しは関数呼び出しのままで、トランザクションも1つのデータベースで完結します。

マイクロサービスとの違いは何ですか?

プロセスとデプロイを分けるかどうかです。マイクロサービスは分けるため、ネットワーク越しの通信・独立したスケール・障害の局所化が得られる代わりに、分散システムの複雑さを負います。判断軸の詳細はマイクロサービスとは?モノリスとの違い・メリット・デメリットと選び方、モジュラモノリスまで整理で整理しています。

モジュールの粒度はどう決めればよいですか?

業務領域の単位です。注文・在庫・請求のように、担当チームや業務の言葉が変わるところが境界になります。層(controller・service・repository)で切ると、機能変更のたびに全モジュールへ手が入るため分割の意味がなくなります。

既存のモノリスから移行できますか?

できます。全体を一度に分けるのではなく、変更頻度の高い領域を1つ選んでモジュール化し、依存検証をCIへ入れます。Packwerkのようなツールは既存の違反を記録した上で新規違反だけを落とせるため、この段階的な進め方に向いています。

データベースも分割する必要がありますか?

物理的な分割は不要です。スキーマ単位で所有権を分け、モジュールをまたぐJOINと外部キー制約をやめれば十分です。DB接続は1つのままでよく、この段階で物理分割すると分散トランザクションの問題だけを先に抱えます。

関連記事

資料請求

RELATED POSTS 関連記事