Spring Modulith: Bessere Monolithen bauen

Viele Entwickler:innen kennen die Situation: Eine zu Beginn überschaubare Anwendung wächst zu einem schwer wartbaren Monolithen heran. Domänenlogik mischt sich mit Infrastrukturcode, Abhängigkeiten werden zunehmend unüberschaubar und die Testbarkeit leidet – ein Hemmschuh für Innovation und schnelle Releases.
Mit Spring Modulith setzen Sie Spring Boot gezielt für Domain-Driven Design (DDD) ein. Das Framework unterstützt Sie dabei, Ihre Anwendung in klar abgegrenzte Module (Bounded Contexts) und Aggregate zu strukturieren, Domain-Events zu nutzen sowie Modul-Abhängigkeiten bereits im Rahmen von Tests oder beim Start der Anwendung automatisch zu verifizieren. So entsteht eine domänenorientierte Architektur mit losen Kopplungen, hoher Kohäsion und expliziten Schnittstellen.
Der modulare Aufbau fördert:
- Klare Abgrenzung von Bounded Contexts: Jedes Modul enthält nur domänenspezifische Logik, Infrastruktur und Schnittstellen für seine Domäne.
- Aggregate und Entitäten: Domain-Modelle bleiben unverfälscht, Geschäftsregeln sind innerhalb der Aggregate gekapselt.
- Domain-Events: Ereignisse visualisieren Zustandsübergänge und ermöglichen entkoppelte Kommunikation zwischen Modulen.
- Automatisierte Architektur-Checks: Spring Modulith validiert zur Kompilierzeit, dass Module nur erlaubte Abhängigkeiten nutzen.
Damit steigern Sie Testbarkeit und Wartbarkeit, beschleunigen Entwicklungszyklen und schaffen eine solide Grundlage für skalierbare, domänenorientierte Spring-Boot-Anwendungen.
Warum modulare Monolithen?
Traditionelle Monolithen kämpfen oft mit zunehmender Komplexität, was zu langsameren Entwicklungszyklen und höheren Wartungskosten führt. Spring Modulith begegnet dem, indem es eine klare Trennung der Belange fördert, die Kopplung zwischen verschiedenen Teilen Ihrer Anwendung reduziert und die Testbarkeit verbessert. Dieser Ansatz ermöglicht die unabhängige Entwicklung und das Testen von Modulen, was schnellere Iterationen und einfacheres Debugging fördert.

Entscheidend ist, dass Spring Modulith architektonische Einschränkungen direkt in Ihre Codebasis einbettet und Ihr Design während der Build- und Test-Pipeline kontinuierlich validiert. Diese "Architektur als Code"-Philosophie verhindert "Architekturdrift" und stellt sicher, dass Ihr System über die Zeit robust und wartbar bleibt.
Einfach gesagt: Ein "Architecture Drift" tritt immer dann auf, wenn Realität und Plan auseinanderlaufen.
Erste Schritte: Projekteinrichtung
Die Initialisierung eines Spring Modulith-Projekts ist unkompliziert. Fügen Sie einfach die wesentlichen Spring Modulith
-Abhängigkeit zu Ihrem pom.xml
hinzu: Dies sind Starter wie spring-modulith-starter-core
, spring-modulith-starter-jpa
, spring-modulith-actuator
, spring-modulith-docs
, spring-modulith-observability
und spring-modulith-starter-test
.
Der Kern der Moduldefinition von Spring Modulith basiert auf der gewählten Paketstruktur. Direkte Unterpakete Ihres Hauptanwendungspakets werden automatisch als Anwendungsmodule erkannt. Wenn sich Ihre Hauptklasse beispielsweise in com.example
befindet, wären com.example.inventory
und com.example.order
eigenständige Module.
Sichtbarkeit steuern: Standardmässig sind Klassen, die sich direkt im Stammverzeichnis eines Moduls befinden, öffentlich. Klassen in Unterpaketen (z.B. internal
, service
) gelten jedoch als privat. Um bestimmte Typen aus einem Unterpaket freizugeben, verwenden Sie die @NamedInterface
-Annotation in einer package-info.java
-Datei innerhalb dieses Unterpakets.
Root-Package = API
Alle public Klassen und Interfaces, die sich direkt im Modul-Stammverzeichnis (also im Basis-Package, z. B.com.example.order
) befinden, gelten als Teil der öffentlichen API und dürfen von anderen Modulen referenziert werden.
Subpackages = intern
Jede Klasse oder jedes Interface in einem Unterpackage (z.B.com.example.order.internal
odercom.example.order.service
) gilt modul-intern und wird von Spring Modulith nicht als öffentliche API angesehen.
Gezielte Freigabe via @NamedInterface
Möchte man bestimmte Typen in einem Unterpackage für andere Module sichtbar machen, erstellt man einepackage-info.java
mit@org.springframework.modulith.NamedInterface("spi")
package com.example.order.spi;
Dadurch wird das Packagespi
(und seine Public-Typen) als benannte Schnittstelle freigegeben
Inter-Modul-Kommunikation: Synchron & Asynchron
Spring Modulith bietet klare Richtlinien, wie Ihre Module interagieren sollten, um eine lose Kopplung zu fördern.
Synchrone Kommunikation: Exponierte APIs
Das direkte Injizieren interner Beans von einem Modul in ein anderes ist unerwünscht und führt zu Verifizierungsfehlern. Stattdessen sollten Module synchron über klar definierte API-Schnittstellen kommunizieren, die direkt im Basispaket des exponierenden Moduls platziert werden. Diese Schnittstellen fungieren als Verträge, die nur die notwendigen Methoden und Datenstrukturen exponieren, während interne Implementierungsdetails privat bleiben.
Beispiel: Verwendung einer API-Schnittstelle
- Order-Modul: API-Interface und DTOs im Wurzel-Package
com.example.order
- Implementierung bleibt in
…order.internal
- Payment-Modul injiziert nur das Interface
// 1. API im Basis-Package
package com.example.order;
import java.util.List;
/**
* Öffentliche API des Order-Moduls.
* Nur Methoden und DTOs, keine internen Details.
*/
public interface OrderApi {
String processOrder(String customerId, List<OrderItem> items);
}
// 2. DTO im Basis-Package
package com.example.order;
/**
* Teil der öffentlichen API: Bestell-Positionen
*/
public class OrderItem {
private final String productId;
private final int quantity;
// Constructor, Getter…
}
// 3. Impl in internem Package
package com.example.order.internal;
import com.example.order.OrderApi;
import com.example.order.OrderItem;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
class OrderApiImpl implements OrderApi {
@Override
public String processOrder(String customerId, List<OrderItem> items) {
// … tatsächliche Business-Logik …
}
}
// 4. Konsument im Payment-Modul
package com.example.payment;
import com.example.order.OrderApi;
import com.example.order.OrderItem;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
@RequiredArgsConstructor
public class PaymentService {
private final OrderApi orderApi; // ✅ nur Interface wird verwendet
public void pay(PaymentRequest req) {
String orderId = orderApi.processOrder(
req.getCustomerId(), req.getItems()
);
// … Zahlung ausführen …
}
}
Dies erzwingt ein "Contract-First"-Design, wodurch stabile Schnittstellen gefördert und Breaking Changes reduziert werden.
Asynchrone Kommunikation: Spring Application Events
Spring Modulith empfiehlt ausdrücklich die Verwendung von Spring Framework Application Events als "primäres Kommunikationsmittel" zwischen Modulen. Dies ermöglicht es Publishern, von Listenern entkoppelt zu bleiben.
Events veröffentlichen: Verwenden Sie die ApplicationEventPublisher
-Bean, um einfache Java-Objekte oder Records als Events zu veröffentlichen, oft innerhalb eines transaktionalen Kontexts.
Beispiel: Veröffentlichen eines Events
package com.example.shipment.service;
import com.example.events.ShipmentCreateEvent;
import org.springframework.context.ApplicationEventPublisher;
@Service
@RequiredArgsConstructor
public class ShipmentService {
private final ApplicationEventPublisher events;
@Transactional
public ShipmentResponse createOrder(ShipmentRequest request, Long customerId) {
//... Sendung speichern...
events.publishEvent(new ShipmentCreateEvent(newShipment.getId())); // Event veröffentlichen
//...
}
}
Events lauschen: Definieren Sie Listener-Methoden, die mit @ApplicationModuleListener
annotiert sind. Diese Annotation vereinfacht die Event-Integration und kann automatisch eine neue Transaktion für die Event-Verarbeitung starten.
Beispiel: Lauschen auf ein Event
package com.example.customer.service;
import com.example.events.ShipmentCreateEvent;
import org.springframework.modulith.ApplicationModuleListener;
@Service
public class CustomerService {
@ApplicationModuleListener
void onShipmentCreateEvent(ShipmentCreateEvent event) {
// Logik zur Verarbeitung des Sendungserstellungsereignisses
System.out.println("Kundenmodul empfing: Sendung erstellt: " + event.orderId());
}
}
Garantierte Zustellung: Spring Modulith erweitert Spring Events um ein "Event Publication Registry", welche die Zustellung von Events durch Persistenz garantiert, selbst wenn die Anwendung abstürzt. Es unterstützt verschiedene Persistenztechnologien (JPA, JDBC, MongoDB) und kann Events an Message Broker wie Kafka externalisieren.
Best Practices & Verifizierung
- Klare Grenzen: Definieren Sie Module in Übereinstimmung mit den Prinzipien des Domain-Driven Design (DDD).
- Abhängigkeitsmanagement: Vermeiden Sie aktiv zyklische Abhängigkeiten. Die Verifizierungstests von Spring Modulith schlagen fehl, wenn Grenzen verletzt werden, und liefern sofortiges Feedback.
Testen
Isoliertes Testen: Verwenden Sie @ApplicationModuleTest
, um einzelne Module isoliert oder mit ihren direkten Abhängigkeiten zu booten und zu testen. Die Scenario
-API bietet eine fluent DSL (Domain Specific Language) zum Testen asynchroner Event-Flüsse.
package example.order;
import com.example.events.OrderCompletedEvent;
import org.springframework.modulith.test.ApplicationModuleTest;
import org.springframework.modulith.test.Scenario;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
@ApplicationModuleTest
class OrderModuleEventTests {
@Test
void whenPlacingOrder_thenPublishOrderCompletedEvent(Scenario scenario) {
scenario.stimulate(() -> orderService.placeOrder("customer-1", "product-1"))
.andWaitForEventOfType(OrderCompletedEvent.class)
.toArriveAndVerify(evt -> assertThat(evt).hasFieldOrPropertyWithValue("customerId", "customer-1"));
}
}
Automatisierte Verifizierung: Integrieren Sie ApplicationModules.verify()
in Ihre JUnit-Tests, um Ihre modulare Struktur kontinuierlich zu validieren.
package com.example.application;
import org.junit.jupiter.api.Test;
import org.springframework.modulith.core.ApplicationModules;
public class ModularityTest {
@Test
void applicationModules() {
ApplicationModules modules = ApplicationModules.of(Application.class);
modules.verify(); // Schlägt fehl, wenn modulare Regeln verletzt werden
}
}
Dokumentation & Observability
Spring Modulith geht nicht nur darum, Regeln durchzusetzen; es geht auch darum, Ihre Architektur zu verstehen.
- Laufzeit-Observability: Durch das Hinzufügen der Abhängigkeiten
spring-modulith-actuator
undspring-modulith-observability
können Sie Ihre Modulstruktur über einen Spring Boot Actuator-Endpunkt (/actuator/modulith
) exponieren und Einblicke in Inter-Modul-Interaktionen durch Micrometer-Spans und OpenTelemetry-Tracing gewinnen. Dies ermöglicht die Visualisierung von Aufruf-Traces in Tools wie Zipkin, was hilft, Leistungsengpässe zu identifizieren und komplexe Event-Flüsse zu verstehen.
Automatisierte Dokumentation: Die Documenter
-Abstraktion kann C4- und UML-Komponentendiagramme sowie tabellarische "Application Module Canvases" direkt aus Ihrer Codebasis generieren. Dies schafft "lebende Dokumentation", die immer aktuell ist. Java
package com.example.application;
import org.junit.jupiter.api.Test;
import org.springframework.modulith.core.ApplicationModules;
import org.springframework.modulith.docs.Documenter;
class DocumentationTests {
ApplicationModules modules = ApplicationModules.of(Application.class);
@Test
void createComponentDiagrams() {
new Documenter(modules)
.writeModulesAsPlantUml() // Generiert systemweites Diagramm
.writeIndividualModulesAsPlantUml(); // Generiert Diagramme pro Modul
}
}
Fazit
Spring Modulith bietet einen pragmatischen und leistungsstarken Ansatz zur Verwaltung der Komplexität in Spring Boot-Anwendungen. Durch die Durchsetzung klarer Modulgrenzen, die Förderung loser Kopplung durch klar definierte Kommunikationsstrategien und die Bereitstellung robuster Tools für Verifizierung, Testen, Dokumentation und Observability trägt es entscheidend zur Wartbarkeit, Testbarkeit und Evolutionsfähigkeit von Anwendungen bei.
Es ist ein unverzichtbares Werkzeug für jeden Spring Boot-Entwickler, der architektonische Disziplin in in seine Projekte bringen möchte.