Je länger eine Spring-Boot-Anwendung lebt, desto größer wird die Gefahr des „Big Ball of Mud“: Services rufen wild andere Services auf, Zyklen entstehen, und aus dem sauberen Monolithen wird ein Wartungs-Albtraum. Spring Modulith 2 adressiert genau dieses Problem: Es strukturiert deine Anwendung in logische Module, erzwingt deren Grenzen und bereitet den Weg für eine eventuelle Microservice-Extraktion — alles ohne das Framework zu wechseln.

Was ist Spring Modulith?

Spring Modulith (aktuell 2.1.0, für Spring Boot 4.1+) verpackt bewährte Architektur-Patterns in Spring-Boot-nativer Form:

  1. Package-basierte Module — Jedes Modul ist ein eigenes Package mit API (öffentlich) und internal-Subpackage (privat)
  2. Verifikations-Tests — Prüfen Einhaltung von Modulgrenzen zur Build-Zeit via ArchUnit
  3. Event-Publikations-Registry — Transaktionale Events mit Wiederholungslogik
  4. Dokumentation — Generiert C4- und UML-Diagramme automatisch
  5. @ApplicationModuleTest — Testet Module in Isolation mit dem Scenario-API

Projekt-Struktur

Eine typische Modulith-Anwendung:

src/main/java/com/shop/
├── Application.java               // @Modulithic
├── order/
│   ├── package-info.java          // @ApplicationModule
│   ├── OrderManagement.java       // öffentliche API
│   ├── OrderCompleted.java        // Domain-Event
│   └── internal/
│       └── OrderInternal.java     // privat, nur im order-Modul nutzbar
└── inventory/
    ├── package-info.java
    └── InventoryManagement.java   // hört auf OrderCompleted
Code-Sprache: JavaScript (javascript)

Setup

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.modulith</groupId>
            <artifactId>spring-modulith-bom</artifactId>
            <version>2.1.0</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-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.modulith</groupId>
        <artifactId>spring-modulith-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>
Code-Sprache: HTML, XML (xml)

Die @Modulithic-Annotation auf der Application-Klasse aktiviert das Modulsystem:

import org.springframework.modulith.Modulithic;

@Modulithic
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
Code-Sprache: JavaScript (javascript)

Module definieren und Abhängigkeiten deklarieren

package-info.java für jedes Modul:

<em>// com/shop/order/package-info.java</em>
@org.springframework.modulith.ApplicationModule(
    displayName = "Bestellungen",
    allowedDependencies = "inventory"
)
package com.shop.order;
Code-Sprache: JavaScript (javascript)
<em>// com/shop/inventory/package-info.java</em>
package com.shop.inventory;
<em>// keine allowedDependencies → darf auf alle exponierten APIs zugreifen</em>
Code-Sprache: HTML, XML (xml)

Events zwischen Modulen

Module kommunizieren ausschließlich via Spring Application Events:

<em>// Im order-Modul: Event publizieren</em>
package com.shop.order;

import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class OrderManagement {
    private final ApplicationEventPublisher events;
    private final OrderRepository orders;

    public OrderManagement(ApplicationEventPublisher events,
                           OrderRepository orders) {
        this.events = events;
        this.orders = orders;
    }

    @Transactional
    public void complete(Long orderId) {
        Order order = orders.findById(orderId).orElseThrow();
        order.markCompleted();
        orders.save(order);
        events.publishEvent(new OrderCompleted(order.getId()));
    }
}

public record OrderCompleted(Long orderId) {}
Code-Sprache: PHP (php)
<em>// Im inventory-Modul: Event empfangen</em>
package com.shop.inventory;

import com.shop.order.OrderCompleted;
import org.springframework.modulith.ApplicationModuleListener;
import org.springframework.stereotype.Component;

@Component
public class InventoryManagement {

    @ApplicationModuleListener
    void on(OrderCompleted event) {
        <em>// Läuft asynchron, in eigener Transaktion,</em>
        <em>// erst nachdem die Original-Transaktion committed wurde</em>
        updateStockForOrder(event.orderId());
    }

    private void updateStockForOrder(Long orderId) {
        <em>// Bestand reduzieren</em>
    }
}
Code-Sprache: JavaScript (javascript)

@ApplicationModuleListener ist ein Shortcut für @Async + @Transactional(propagation = REQUIRES_NEW) + @TransactionalEventListener — perfekt für lose gekoppelte Module.

Modul-Verifikationstests

Spring Modulith generiert während des Builds automatisch Verifikationen. Für eigene, explizite Tests:

import org.junit.jupiter.api.Test;
import org.springframework.modulith.core.ApplicationModules;

class ModularityTests {

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

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

    @Test
    void noCycles() {
        modules.detectViolations().throwIfPresent();
    }
}
Code-Sprache: JavaScript (javascript)

Das erkennt:

  • Zyklen zwischen Modulen
  • Zugriffe auf *.internal.*-Packages von außen
  • Verstöße gegen allowedDependencies

Modul-Isolierte Integrationstests mit @ApplicationModuleTest

package com.shop.order;

import org.junit.jupiter.api.Test;
import org.springframework.modulith.test.ApplicationModuleTest;
import org.springframework.modulith.test.PublishedEvents;
import org.springframework.modulith.test.Scenario;
import org.springframework.beans.factory.annotation.Autowired;

@ApplicationModuleTest  <em>// Bootet nur das order-Modul</em>
class OrderIntegrationTests {

    @Autowired OrderManagement orders;

    @Test
    void completingOrderPublishesEvent(PublishedEvents events) {
        orders.complete(1L);

        events.ofType(OrderCompleted.class)
              .matching(e -> e.orderId().equals(1L))
              .hasSize(1);
    }

    @Test
    void asyncScenarioExample(Scenario scenario) {
        scenario.publish(new OrderCompleted(42L))
                .andWaitForEventOfType(StockUpdated.class)
                .matching(e -> e.orderId().equals(42L))
                .toArriveAndVerify(event -> {
                    assertThat(event.newStockLevel()).isGreaterThan(0);
                });
    }
}
Code-Sprache: JavaScript (javascript)

Der Scenario-API ist das Herzstück asynchroner Modul-Tests: publish()/stimulate() → Event erwarten → Zustandsänderung verifizieren.

Automatische Dokumentation

import org.junit.jupiter.api.Test;
import org.springframework.modulith.docs.Documenter;

class DocumentationTests {

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

    @Test
    void generateDocs() {
        new Documenter(modules)
            .writeModulesAsPlantUml()          <em>// System C4 Diagramm</em>
            .writeIndividualModulesAsPlantUml() <em>// Pro-Modul Diagramm</em>
            .writeModuleCanvases()              <em>// Tabellarische Modul-Übersichten</em>
            .writeAggregatingDocument();        <em>// Alles-in-einem AsciiDoc</em>
    }
}
Code-Sprache: JavaScript (javascript)

Die Dateien landen in target/spring-modulith-docs/ und enthalten C4-Diagramme (PlantUML), Modul-Canvases (Spring Beans, Events, Properties) und ein aggregierendes AsciiDoc.

Der Weg zu Microservices

Spring Modulith unterstützt den schrittweisen Übergang zum Microservice:

  1. Heute: Monolith mit Modul-Grenzen und internen Events
  2. Morgen: Events mit @Externalized annotieren → Kafka/AMQP@Externalized("order-completed::#{#this.orderId()}") public record OrderCompleted(Long orderId) {}
  3. Übermorgen: Modul als eigener Service extrahieren, Events vom Broker konsumieren

Keine Code-Änderung innerhalb des Moduls nötig.

Fazit

Spring Modulith 2 ist kein neues Framework — es ist ein Architektur-Werkzeugkasten für Spring Boot, der saubere Modulgrenzen zur Build-Zeit erzwingt und asynchrone Event-Kommunikation fördert. Die automatische C4-Dokumentation ist ein netter Bonus, der gerade in wachsenden Teams Gold wert ist.

Für jedes Spring-Boot-Projekt mit mehr als 2 Modulen: Setzt Spring Modulith ein, bevor der Monolith unwartbar wird. Euer zukünftiges Ich wird es euch danken.