Spring

Spring Modulithとは?使い方・構造検証・イベント連携を2.1.2の実行結果で解説【2026年10月】

Spring Modulithとは?使い方・構造検証・イベント連携を2.1.2の実行結果で解説【2026年10月】

Spring Modulithは、Spring Bootアプリを1つのデプロイ単位のまま複数のモジュールに分け、その境界をテストで検証するSpring公式プロジェクトです。2026年10月28日時点の最新安定版は2026年10月23日リリースの2.1.2(Spring Boot 4.1.2・Spring Framework 7.0.9ベース)で、同日に次期版の2.2.0-RC1も公開されています。本記事では、Spring Boot 4.1.2とSpring Modulith 2.1.2で最小構成のアプリを実際にビルドし、verify()が出す違反メッセージ、イベント記録テーブルの中身、失敗イベントの再送までを実行結果で示します。

まとめ:Spring Modulithの要点(2.1.2時点)

  • 何をするものか:@SpringBootApplicationのパッケージ直下のサブパッケージを1モジュールとみなし、ApplicationModules.of(...).verify()で循環依存と非公開型への参照を検出する。
  • 版の選び方:Spring Boot 4.1なら2.1.2、4.0なら2.0.9、3.5なら1.4.14。Spring Boot 4.2向けの2.2は2026年10月23日にRC1が出た段階。
  • イベント連携:@ApplicationModuleListenerで受け、永続化スターターを入れると発行イベントがevent_publicationテーブルに残る。リスナが例外を投げた行はFAILEDになり、APIで再送できる。
  • テスト:@ApplicationModuleTestで1モジュールだけを起動し、Scenarioでイベントの到着を検証する。
  • 注意点:allowedDependencies = "order :: *"は相手モジュールのbaseパッケージを含まない。2.1系で全公開範囲を許すなら"order"と"order :: *"の両方を書く。

Spring Modulithとは:モジュラーモノリスの境界をテストで守る仕組み

モノリスは育つほど内部の依存が絡まり、どこを変えると何が壊れるかが見えなくなります。マイクロサービスはこれをサービス分割で解きますが、ネットワーク通信・サービスごとのDB・分散トランザクションの運用を抱えます。Spring Modulithは単一プロセス・単一DBのまま論理的なモジュール境界を引き、その境界をテストで守る中間解です。検証の内部ではArchUnitを使っています。モジュラーモノリスという設計そのものはモジュラモノリスとは?モジュール境界の設計とDB分割・依存検証の実装で扱っており、本記事はSpring Modulithの使い方に絞ります。

マイクロサービスとの違い

観点 Spring Modulith マイクロサービス
デプロイ単位 1プロセス・1デプロイ サービスごとに独立
モジュール間通信 メソッド呼び出し/アプリ内イベント ネットワーク越しのAPI
データ 単一DB・ローカルトランザクション サービスごとのDB・結果整合
境界の強制 verify()のテスト プロセス分離

境界を守れているモジュールは、後でサービスとして切り出すときの依存の絡まりが少なくなります。

バージョンとSpring Bootの対応(2026年10月28日時点)

Spring Modulithの版は、使っているSpring Bootの世代で決まります。spring.ioの世代APIは各世代を1つのSpring Boot世代に対応づけており、最新パッチはGitHubのリリース一覧で確認できます。

Spring Modulith Spring Boot 最新版(リリース日)
2.2.x 4.2 2.2.0-RC1(2026-10-23)
2.1.x 4.1 2.1.2(2026-10-23)
2.0.x 4.0 2.0.9(2026-10-23)
1.4.x 3.5 1.4.14(2026-10-23)
1.3.x 3.4 1.3.12

Maven Centralのspring-modulith-coreのPOMでは、2.1.2がspring-boot-autoconfigure 4.1.2、1.4.14が3.5.16に依存しています。1.4系はSpring Boot 3.5専用で、3.3や3.4には1.2系・1.3系が対応します。Spring Boot 3.5のOSSサポートは2026年6月30日に終わっているため、新規に始めるなら4.1と2.1.2の組み合わせを選びます。Spring Boot自体の版ごとの期限はSpring Bootバージョン一覧とサポート期限、4系の変更点はSpring Boot 4とは?最新バージョン4.1の変更点にまとめています。

Spring Modulithを導入する:Initializr・Maven・Gradle

Spring Initializrでの依存追加

Spring Initializrでは「Developer Tools」の中に「Spring Modulith」があります。2026年10月28日時点のメタデータでは対象がSpring Boot 4.0以上4.3未満で、選べるBootは4.0系・4.1系・4.2系だけです。Spring Boot 3.5のプロジェクトに入れる場合は、下のBOMを手で書き、版を1.4.14にします。

MavenとGradleの依存定義

Spring Boot 4.1.2のプロジェクトを用意したうえで、Modulithの版をBOMでそろえ、必要なスターターを追加します。以下は既存のビルド設定へ追加する断片で、Modulithの基本構成としてコアとテストの2つを示します。

<!-- pom.xml -->
<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.springframework.modulith</groupId>
      <artifactId>spring-modulith-bom</artifactId>
      <version>2.1.2</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

<dependencies>
  <dependency>
    <groupId>org.springframework.modulith</groupId>
    <artifactId>spring-modulith-starter-core</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.modulith</groupId>
    <artifactId>spring-modulith-starter-test</artifactId>
    <scope>test</scope>
  </dependency>
</dependencies>

Gradleでは、公式リファレンスと同じくio.spring.dependency-managementプラグインでBOMを取り込みます。

// build.gradle
dependencyManagement {
  imports {
    mavenBom 'org.springframework.modulith:spring-modulith-bom:2.1.2'
  }
}

dependencies {
  implementation 'org.springframework.modulith:spring-modulith-starter-core'
  testImplementation 'org.springframework.modulith:spring-modulith-starter-test'
}

用途別のスターター

用途 アーティファクト
イベント記録をRDBに残す spring-modulith-starter-jdbc/-jpa
イベント記録をNoSQLに残す spring-modulith-starter-mongodb/-neo4j
Actuator・メトリクス・トレース spring-modulith-starter-insight
モジュール図の生成 spring-modulith-docs(test)
Kafkaへのイベント外部化 spring-modulith-events-kafka

モジュールを定義する:パッケージ構造と公開範囲

モジュールは設定ファイルではなくパッケージ構造で決まります。以下の構成でApplicationModulesを出力すると、orderとinventoryの2モジュールとして認識されました。

com.example.shop
├─ ShopApplication.java        @SpringBootApplication
├─ order                       order モジュール
│   ├─ OrderManagement.java    公開(他モジュールから参照可)
│   ├─ OrderCompleted.java     公開イベント
│   └─ internal
│       └─ OrderValidator.java 非公開
└─ inventory                   inventory モジュール
    └─ InventoryManagement.java
# Order
> Logical name: order
> Base package: com.example.shop.order
> Excluded packages: none
> Spring beans:
  + ….OrderManagement
  o ….internal.OrderValidator

出力の+は公開Bean、oは非公開Beanです。モジュールのbaseパッケージ直下にあるpublicな型が公開API、internalのようなサブパッケージの型は非公開になります。サブパッケージを公開したいときは、そのパッケージのpackage-info.javaに@NamedInterface("spi")を付けて名前付きインターフェースにします。

allowedDependenciesによる依存先の制限

依存してよい相手はpackage-info.javaの@ApplicationModuleで宣言します。

@ApplicationModule(allowedDependencies = "order")
package com.example.shop.inventory;

import org.springframework.modulith.ApplicationModule;

ここで"order :: *"と書くと、orderのbaseパッケージにある型は許可されません。inventoryがorder.OrderCompletedを受け取る構成で試すと、"order"は通り、"order :: *"は次の違反で失敗しました。

Module 'inventory' depends on module 'order' via com.example.shop.inventory.InventoryManagement -> com.example.shop.order.OrderCompleted. Allowed targets: order :: *.

::の右側は名前付きインターフェースの指定で、*は「明示的に宣言した名前付きインターフェースすべて」を指します。2.1系でbaseパッケージと名前付きインターフェースの両方を許すなら{"order", "order :: *"}と2つ書きます。2.2では両方をまとめる"order :: **"が加わりました(#1924)。宣言を書かなければ依存先モジュールの追加制限はありませんが、公開範囲と循環依存の規則は引き続き適用されます。依存先をさらに絞りたいモジュールにだけ宣言を書けば足ります。

既存プロジェクトを段階的に移す:Openモジュール

パッケージ構成を一度に直せない既存プロジェクトでは、@ApplicationModule(type = Type.OPEN)を付けたモジュールを作れます。Openモジュールは内部の型にも他モジュールからアクセスでき、サブパッケージの型も含めて公開扱いになります。公式リファレンスのFundamentalsは、この機能を推奨構成へ少しずつ移すための手段と位置づけ、完全にモジュール化したアプリでOpenモジュールを使うのはパッケージ構成がまずいサインだと書いています。移行の途中だけ使い、移し終えたら外すのが前提です。

verify()によるモジュール構造の検証

境界が守られているかは、テスト1本で判定します。

class ModularityTests {

  ApplicationModules modules = ApplicationModules.of(ShopApplication.class);

  @Test
  void verifiesModularStructure() {
    modules.verify();
  }
}

違反時に出るメッセージ

inventoryからorder.internal.OrderValidatorをコンストラクタ注入して呼ぶと、テストはorg.springframework.modulith.core.Violationsで失敗し、参照箇所ごとに理由が出ます(抜粋)。

- Module 'inventory' depends on non-exposed type com.example.shop.order.internal.OrderValidator within module 'order'!
Method <com.example.shop.inventory.InventoryManagement.on(com.example.shop.order.OrderCompleted)> calls method <com.example.shop.order.internal.OrderValidator.validate(java.lang.String)> in (InventoryManagement.java:14)

この例ではコンストラクタ・フィールド・メソッド呼び出し・コンストラクタ引数の4か所が別々に報告されました。行番号付きなので、どこを直せばよいかがすぐ分かります。

次にorderからinventoryのBeanを注入すると、循環依存として報告されます。

- Cycle detected: Slice inventory ->
                Slice order ->
                Slice inventory
  1. Dependencies of Slice inventory
    - Method <com.example.shop.inventory.InventoryManagement.on(com.example.shop.order.OrderCompleted)> has parameter of type <com.example.shop.order.OrderCompleted> in (InventoryManagement.java:0)
  2. Dependencies of Slice order
    - Field <com.example.shop.order.OrderManagement.inventory> has type <com.example.shop.inventory.InventoryManagement> in (OrderManagement.java:0)

見落としやすいのは1の方です。イベントリスナの引数にOrderCompletedを取るだけで、inventoryはorderに依存していると数えられます。イベント連携は依存をなくす仕組みではありません。依存の向きを「受け手が送り手のイベント型を知る」一方向にそろえる仕組みです。逆向きにBeanを呼んだ時点で循環になります。

違反を一部だけ無視したいときは、modules.detectViolations().filter(...).throwIfPresent()のようにViolations#filterで絞ってから例外にします。

モジュール間をイベントでつなぐ:@ApplicationModuleListener

公式リファレンスのイベントの章は、モジュール同士をできるだけ疎結合に保つため、イベントの発行と受信を主な連携手段にするよう勧めています。公開Beanの同期呼び出しも禁止ではないので、結果をその場で受け取る必要がある処理はBean呼び出し、他モジュールへの通知はイベントと使い分けます。送り手はApplicationEventPublisherで発行し、受け手は@ApplicationModuleListenerで受けます。

@Service
public class OrderManagement {
  // コンストラクタ省略
  @Transactional
  public void complete(String orderId) {
    validator.validate(orderId);
    events.publishEvent(new OrderCompleted(orderId));
  }
}

@Service
public class InventoryManagement {
  @ApplicationModuleListener
  void on(OrderCompleted event) {
    // 発行元のコミット後に、別スレッド・新しいトランザクションで実行される
  }
}

@ApplicationModuleListenerは@Async・@Transactional(propagation = REQUIRES_NEW)・@TransactionalEventListenerを合成したアノテーションです。非同期実行が有効な構成では、注文のトランザクションがコミットしてから、在庫側が非同期・別トランザクションで動きます。後述の永続化スターターは非同期実行も自動設定します。それを使わずに実装する場合は、@EnableAsyncなどによる非同期実行の有効化と、トランザクションマネージャーの設定が必要です。イベント駆動の設計全般はイベント駆動アーキテクチャとは?仕組み・実装パターンと採用判断を実装者目線で解説で扱っています。

Event Publication Registryに残る記録

非同期リスナは、処理中にアプリが落ちるとイベントを取りこぼします。これを防ぐのがEvent Publication Registryです。spring-modulith-starter-jdbcとH2を入れ、spring.modulith.events.jdbc.schema-initialization.enabled=trueで起動するとEVENT_PUBLICATIONテーブルが作られ、次の列を持っていました。

ID, COMPLETION_DATE, EVENT_TYPE, LISTENER_ID, PUBLICATION_DATE,
SERIALIZED_EVENT, STATUS, COMPLETION_ATTEMPTS, LAST_RESUBMISSION_DATE

リスナ1件ごとに1行が入り、イベント本体はJSON({"orderId":"A-1"})で保存されます。成功した行はSTATUS=COMPLETEDで完了日時が入り、リスナが例外を投げた行はSTATUS=FAILED・COMPLETION_DATE=nullで残りました。状態は2.0からPUBLISHED・PROCESSING・COMPLETED・FAILED・RESUBMITTEDの5つです。

失敗イベントの再送APIと監視設定

再送の手段は3つあります。

  • 起動時の自動再送:spring.modulith.events.republish-outstanding-events-on-restart=true。既定はfalseで、公式は複数インスタンス構成では他のインスタンスが処理中のイベントまで再送しうるため勧めていません。
  • APIで再送:IncompleteEventPublications(未完了すべて)か、2.0で入ったFailedEventPublications(FAILEDだけ)を注入して呼ぶ。
  • 止まった行を失敗扱いにする:spring.modulith.events.staleness.*で状態ごとの期限を決めると、2.0で入ったStaleness Monitorが期限切れの行をFAILEDにする。2.1.2では状態別の期限がすべて未設定なら監視は動かない。設定する期限は正の値が必要で、0や負の値は拒否される。

リスナが失敗した後に原因を取り除き、incomplete.resubmitIncompletePublicationsOlderThan(Duration.ZERO)を呼ぶと、FAILEDの行はCOMPLETEDに変わり、COMPLETION_ATTEMPTSが1から2に増えました。処理試行回数を列で追えるので、失敗状態と組み合わせて「3回失敗したら人が見る」といった運用ルールを組めます。また、2.1系ではリスナ処理が成功しても完了記録だけが失敗する可能性があるため、再送時に業務処理が重複しても問題が起きない設計が必要です。

完了済みの行を残すか消すか:completion-mode

既定のupdateでは完了した行も消えずに残るため、テーブルが増え続けます。spring.modulith.events.completion-modeで扱いを変えられます。

値 完了時の動き
update(既定) 完了日時を入れて行を残す
delete 行を削除する
archive アーカイブ用テーブルへ移す

deleteにして注文を完了させると、該当イベントの行は0件になりました。監査のために完了履歴を見たいならarchive、記録が不要ならdelete、どちらでもなければupdateのままCompletedEventPublicationsで古い行を定期的に消します。

イベントの外部化とoutbox

イベント型に@Externalizedを付けると、Kafka・AMQP・JMS・Spring Messagingへイベントを流せます。

@Externalized("order.completed::#{#this.orderId()}")
public record OrderCompleted(String orderId) {}

既定の外部化は非同期リスナで送る簡易方式で、公式も本来のoutboxパターンにある機能は備えていないと書いています。2.1からはspring.modulith.events.externalization.mode=outboxでNamastack OutboxかJobRunrに配送を任せられ、Namastackは現在リレーショナルDBのみの対応です。outboxの考え方はOutboxパターンとは?DB更新とイベント発行を一致させる実装方式を解説を参照してください。

モジュール単位で結合テストする:@ApplicationModuleTestとScenario

@ApplicationModuleTestを付けたテストクラスは、そのクラスのパッケージにあるモジュールだけを起動します。次のテストクラスはcom.example.shop.orderパッケージに置いたもので、既定の起動範囲ではorderだけを起動し、OrderCompletedの発行を確かめます。実行して成功しました。

@ApplicationModuleTest
class OrderModuleTests {

  @Autowired OrderManagement orders;

  @Test
  void publishesEventOnCompletion(Scenario scenario) {
    scenario.stimulate(() -> orders.complete("A-1"))
        .andWaitForEventOfType(OrderCompleted.class)
        .matching(event -> event.orderId().equals("A-1"))
        .toArriveAndVerify(event -> assertThat(event.orderId()).isEqualTo("A-1"));
  }
}

Scenarioはstimulateで処理を起こし、andWaitForEventOfTypeで対象イベントの発行を待ちます。この例は注文イベントの発行を検証するもので、在庫リスナの処理完了までは確認しません。非同期処理の結果を検証する場合は、処理後のイベントかandWaitForStateChangeによる状態変化を待ちます。起動範囲は引数で変えます。

BootstrapMode 起動するモジュール
STANDALONE(既定) 対象モジュールのみ
DIRECT_DEPENDENCIES 対象+直接依存するモジュール
ALL_DEPENDENCIES 対象+依存ツリー全体

STANDALONEで他モジュールのBeanが要るときは、@MockitoBeanでモックに置き換えます。DBやブローカーをコンテナで用意するなら、@ServiceConnectionとは|Spring BootのTestcontainers接続設定を自動化する方法の手順と組み合わせられます。

DocumenterとActuatorによるモジュール構成の可視化

spring-modulith-docsをテスト依存に足し、次のテストを実行します。

new Documenter(modules)
    .writeModulesAsPlantUml()
    .writeIndividualModulesAsPlantUml()
    .writeModuleCanvases();

Mavenではtarget/spring-modulith-docsに、全体図のcomponents.puml、モジュールごとのmodule-order.puml・module-inventory.puml、モジュールの公開Bean・イベント・依存を表にしたCanvas(module-order.adocなど)が出力されました。文書生成テストを実行した時点のモジュール構造が図に反映されます。CIで生成を継続すれば、手作業で更新する設計図とのずれを減らせます。まとめて出すならwriteDocumentation()も使えます。

稼働中のWebアプリでは、spring-modulith-starter-insightを追加し、management.endpoints.web.exposure.include=health,modulithでHTTP公開対象に含めると/actuator/modulithがモジュール構成をJSONで返します。あわせてモジュールの公開APIの呼び出しがMicrometerのスパンになり、イベント発行数がmodule.events.publishedというカウンタで集計されます(公式リファレンスのProduction-ready Features)。

2.2で変わる点:アップグレード前に確認したい挙動

2.2はSpring Boot 4.2に合わせた系列で、spring.ioの世代APIでは2026年11月が初回リリースの予定です。RC1までに入った変更のうち、既存コードの挙動に関わるものを挙げます。

変更 影響 issue
イベント完了の記録をリスナのトランザクション内で行う 完了記録だけ失敗する隙間が減る #1961
"order :: **"の追加 baseパッケージ込みの依存を1行で書ける #1924
Scenarioで「届かないこと」を検証 イベントが出ないケースをテストできる #1357
失敗と打ち切り(abandoned)の区別 再送をあきらめた行を区別できる #1764

#1961は、2.1系まではリスナのコミット後に完了を別途記録しており、リスナは成功したのに完了の記録だけ失敗する可能性があったものを、リスナのトランザクション内へ移す変更です。リスナがトランザクション外や読み取り専用で動く場合は従来の動きに戻ります(issue #1961)。実装ではspring.modulith.events.completionの既定値がin-listenerになり、in-interceptorを指定すると2.1系までの動きに戻せます。完了記録のタイミングに依存した処理を書いているなら、2.2のGA後に検証してから上げます。本番で使うのはGA版の2.1.2です。

Spring Modulithを採用すべき場面と向かない場面

向いているのは、1つのチームが1つのSpring Bootアプリを育てていて、注文・在庫・請求のように機能の境目が見えている中規模以上のプロジェクトです。境界をテストで守っておけば、負荷や組織の都合で分けたくなったモジュールだけを後からサービスにできます。ドメイン駆動設計(DDD)の境界づけられたコンテキストをパッケージに落とす土台にもなります。

次の条件に当てはまる場合は、導入しても効果が出にくいと判断します。

  • パッケージをcontroller/service/repositoryで切っている:Spring Modulithは機能単位のパッケージを前提にしており、技術レイヤ別のままでは全クラスが3つのモジュールに入るだけで検証の意味がない。先に機能単位へ組み替える(考え方はVertical Slice Architecture(バーティカルスライス)とは?が近い)。
  • 機能が1つしかない小さなアプリ:分ける境界が無い段階では、モジュール構造を検証する利点が小さい。なお、イベント記録テーブルの保守が発生するのは、永続化スターターを採用した場合に限られる。
  • イベントの取りこぼし防止が目的なのに記録用のDBを置けない:再送の仕組みは永続化スターター前提で、記録先が無いと防げない。構造検証・モジュール単位テスト・文書生成はDBなしで使えるので、目的がそちらなら導入できる。
  • すでにサービスへ分割済み:各サービスが十分小さいなら、サービス内をさらにモジュールに分ける利点は小さい。

モノリスを機能単位で設計し直す段階から相談したい場合は、フルスクラッチ開発でJava・Spring Bootの設計と実装を支援しています。

よくある質問

Spring Modulithの最新バージョンは?

2026年10月28日時点の最新安定版は、2026年10月23日リリースの2.1.2です。Spring Boot 4.1系に対応します。同日に2.2.0-RC1(Spring Boot 4.2.0-RC1ベース)も出ていますが、こちらはリリース候補版です。

Spring Boot 3でもSpring Modulithは使える?

使えます。Spring Boot 3.5なら1.4系(最新1.4.14)、3.4なら1.3系を選びます。ただしSpring Initializrでは4.0以上しか選べないため、BOMを手で追加します。Spring Boot 3.5のOSSサポートは2026年6月30日に終了しています。

@ApplicationModuleListenerと@EventListenerの違いは?

@EventListenerは発行と同じスレッド・同じトランザクションで即座に動きます。@ApplicationModuleListenerは発行元のコミット後に、別スレッド・新しいトランザクションで動きます。永続化スターターと組み合わせると、失敗したイベントがFAILEDとして残り、再送できます。

Spring Modulithのサンプルコード(example)はどこにある?

公式リポジトリのspring-modulith-examplesにあります。全機能を使うspring-modulith-example-fullのほか、JDBC・MongoDB・Neo4jへのイベント記録、Kafkaへの外部化、outboxの例が分かれています。mainブランチは次期版の開発中なので、使っている版のタグに切り替えて読みます。

Spring Modulithからマイクロサービスへ移行できる?

できます。モジュール間をイベントでつなぎ、verify()で境界を守っていれば、切り出すモジュールのイベントを@ExternalizedでKafkaなどへ流し、受け手を別サービスに移す形で分けられます。Beanを直接呼んでいる箇所が多いほど、切り出し時にAPI化する手間が増えます。

関連記事

お気に入りに入れた記事の一覧

一創の公式 SNS

X では毎朝の新着記事と IT ニュースの速報を、YouTube・TikTok・Instagram・LinkedIn・Facebook では記事の解説動画を配信しています。

この記事は以下の記事からリンクされています

資料請求

RELATED POSTS 関連記事

目次