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:
- Package-basierte Module — Jedes Modul ist ein eigenes Package mit API (öffentlich) und
internal-Subpackage (privat) - Verifikations-Tests — Prüfen Einhaltung von Modulgrenzen zur Build-Zeit via ArchUnit
- Event-Publikations-Registry — Transaktionale Events mit Wiederholungslogik
- Dokumentation — Generiert C4- und UML-Diagramme automatisch
@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:
- Heute: Monolith mit Modul-Grenzen und internen Events
- Morgen: Events mit
@Externalizedannotieren → Kafka/AMQP@Externalized("order-completed::#{#this.orderId()}") public record OrderCompleted(Long orderId) {} - Ü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.