Viele Spring-Boot-Anwendungen beginnen übersichtlich und wachsen dann zu einem schwer wartbaren Monolithen heran. Spring Modulith setzt genau dort an: Es hilft, eine Anwendung fachlich in klar abgegrenzte Module zu teilen, ihre Abhängigkeiten zu prüfen und sie gezielt zu testen. Ich zeige, wie das Konzept funktioniert, wann es gegenüber Microservices sinnvoller ist und welche Fehler bei der Umsetzung häufig auftreten.
Die wichtigsten Fakten für eine modulare Spring-Anwendung
- Modularer Monolith bedeutet eine Anwendung mit klaren fachlichen Grenzen, aber zunächst nur einem Deployment.
- Package-Strukturen bilden standardmäßig die Grundlage für die Erkennung von Anwendungmodulen.
- ApplicationModules.verify() kann unerlaubte Abhängigkeiten zwischen Modulen frühzeitig sichtbar machen.
- Application Events ermöglichen eine lose Kopplung, ohne sofort einen Message Broker einzuführen.
- @ApplicationModuleTest und die Scenario-API erleichtern gezielte Integrationstests auf Modulebene.
Was Spring Modulith in einer Spring-Boot-Anwendung verändert
Das Framework ist kein neuer Webserver und auch kein Ersatz für Spring Boot. Es ergänzt das Spring-Ökosystem um Werkzeuge für fachliche Modularisierung, Architekturprüfung, Integrationstests und technische Dokumentation.
Die Anwendung bleibt dabei zunächst ein einzelnes deploybares System. Die Module liegen im selben Prozess und können dieselbe Datenbank verwenden. Der Unterschied zu einem klassischen Monolithen liegt in den bewusst definierten Grenzen, die nicht nur auf einem Architekturdiagramm existieren, sondern im Code überprüfbar werden.
In der Praxis bedeutet das beispielsweise eine Aufteilung in Module wie bestellung, katalog, zahlung und versand. Jedes Modul besitzt eine öffentliche Schnittstelle, während interne Services, Repositories und Implementierungsdetails verborgen bleiben. Genau diese Trennung macht spätere Änderungen deutlich berechenbarer.
Ich halte diesen Ansatz besonders für Teams interessant, die Microservices zwar fachlich erwägen, aber den zusätzlichen Betrieb nicht rechtfertigen können. Ein modularer Monolith liefert bereits viele Architekturvorteile, ohne dass sofort mehrere Deployments, Netzwerkfehler und verteilte Transaktionen hinzukommen.
Wie Anwendungsmodule und Abhängigkeiten aufgebaut werden
Spring Modulith leitet Module standardmäßig aus der Package-Struktur einer Anwendung ab. Liegt die Hauptklasse beispielsweise unter de.metawebart.shop, können darunterliegende Packages wie order, catalog oder inventory als fachliche Module erkannt werden.
Ein Modul sollte seine öffentliche API bewusst klein halten. In vielen Projekten reicht ein Package wie order für interne Komponenten und ein Unterpackage wie order.api für Klassen, die andere Module verwenden dürfen. Dadurch wird sichtbar, welcher Code stabil bleiben muss und welcher jederzeit geändert werden kann.
Die Architektur automatisch prüfen
Die Modulstruktur lässt sich mit ApplicationModules analysieren und verifizieren. Ein typischer Test sieht vereinfacht so aus:
class ModularityTests {
@Test
void verifiesModularStructure() {
ApplicationModules.of(Application.class).verify();
}
}Der Test kann unter anderem unerlaubte Zugriffe auf interne Packages oder problematische Zyklen zwischen Modulen melden. Das ist kein vollständiger Ersatz für Architekturentscheidungen, aber ein sehr wirksames Sicherheitsnetz im Build-Prozess.
Meine Empfehlung ist, diesen Test früh einzurichten und in die Continuous Integration aufzunehmen. Eine Regel, die erst nach mehreren Jahren gewachsenem Code eingeführt wird, erzeugt meist viele Warnungen auf einmal und verliert dadurch an Akzeptanz.
Warum Application Events für die Kopplung entscheidend sind
Direkte Aufrufe sind innerhalb eines Moduls völlig sinnvoll. Zwischen fachlichen Modulen führen sie jedoch oft zu langen Abhängigkeitsketten. Ein Bestellmodul sollte beispielsweise nicht direkt den internen E-Mail-Service des Versandmoduls aufrufen müssen.
Stattdessen kann es ein fachliches Ereignis wie OrderPlaced veröffentlichen. Das Versandmodul reagiert darauf mit einem eigenen Listener. Die Bestellung kennt dann nur das Ereignis, nicht die konkrete Implementierung des Empfängers. Diese Form der Kommunikation reduziert strukturelle Kopplung und macht fachliche Abläufe leichter erweiterbar.
public record OrderPlaced(UUID orderId) {}
@Service
class OrderService {
private final ApplicationEventPublisher events;
void placeOrder(UUID orderId) {
// Bestellung speichern
events.publishEvent(new OrderPlaced(orderId));
}
}
@Component
class ShippingHandler {
@ApplicationModuleListener
void prepareShipment(OrderPlaced event) {
// Versandprozess starten
}
}Bei Datenbanktransaktionen ist der Zeitpunkt der Veröffentlichung wichtig. Ein Listener sollte nicht reagieren, bevor die Bestellung tatsächlich erfolgreich gespeichert wurde. Transaktionale Event-Verarbeitung und die Ereignis-Publikationsregistrierung helfen dabei, dieses Problem kontrolliert zu behandeln.
Events lösen allerdings nicht automatisch jede Integrationsfrage. Sobald Nachrichten über Prozessgrenzen hinweg zuverlässig zugestellt werden müssen, kommen zusätzliche Mechanismen wie Persistenz, Wiederholungen oder ein externer Broker ins Spiel. Für Kommunikation innerhalb eines einzelnen Prozesses reicht der Spring-Event-Mechanismus häufig aus.
So lässt sich das Konzept schrittweise einführen
Für ein neues Projekt würde ich zunächst die fachlichen Verantwortungsbereiche skizzieren und erst danach die Packages anlegen. Technische Kategorien wie controller, service und repository sind dafür meist zu grob, weil sie die eigentliche Domäne nicht abbilden.
- Geschäftsbereiche bestimmen, etwa Bestellung, Kunde, Rechnung und Versand.
- Packages nach diesen Bereichen strukturieren und interne Komponenten zunächst innerhalb des Moduls halten.
- Öffentliche Schnittstellen definieren, statt beliebige Klassen aus anderen Modulen zu importieren.
- Modulgrenzen mit ApplicationModules.verify() in einem Test festschreiben.
- Direkte Querverbindungen reduzieren und geeignete fachliche Events einführen.
- Module einzeln testen, bevor die gesamte Anwendung in jedem Test vollständig gestartet wird.
Für bestehende Anwendungen ist eine komplette Umstrukturierung selten die beste erste Maßnahme. Ich würde mit einem fachlich gut verständlichen Bereich beginnen, beispielsweise dem Zahlungsprozess, und dort die Abhängigkeiten sichtbar machen. Danach kann das Team schrittweise weitere Teile herauslösen.
Gezielt auf Modulebene testen
Mit @ApplicationModuleTest lässt sich ein Integrationstest auf ein einzelnes Modul oder einen definierten Modulbereich fokussieren. Die Scenario-API unterstützt dabei Tests, die einen Ausgangszustand herstellen, ein Ereignis auslösen und anschließend auf ein Ereignis oder eine Zustandsänderung warten.
Das ist besonders nützlich bei asynchronen Abläufen. Statt im Test mit festen Wartezeiten zu arbeiten, kann geprüft werden, ob ein erwartetes Ereignis innerhalb eines sinnvollen Zeitfensters eingetroffen ist. Dadurch werden Tests aussagekräftiger und weniger abhängig von der Geschwindigkeit der lokalen Entwicklungsumgebung.
Spring Modulith kann außerdem aus der erkannten Struktur Dokumentationsfragmente erzeugen. Solche Diagramme ersetzen kein Architekturgespräch, zeigen aber schnell, ob die tatsächlichen Abhängigkeiten noch dem fachlichen Modell entsprechen.
Modularer Monolith oder Microservices
Die beiden Ansätze sind keine einfachen Gegenspieler. Ein modularer Monolith konzentriert sich auf klare Grenzen innerhalb eines Deployments, während Microservices zusätzlich unabhängige Prozesse, Infrastruktur und Betriebsverantwortung voraussetzen.
| Kriterium | Modularer Monolith | Microservices |
|---|---|---|
| Deployment | Eine Anwendung | Mehrere unabhängig deploybare Services |
| Kommunikation | Methodenaufrufe und In-Process-Events | Netzwerk, Messaging oder APIs |
| Datenhaltung | Gemeinsame oder logisch getrennte Datenbank | Idealerweise eigene Datenhaltung je Service |
| Betrieb | Geringerer Infrastrukturaufwand | Höherer Aufwand für Monitoring, Deployment und Fehleranalyse |
| Skalierung | Primär als Gesamtsystem | Einzelne Services separat skalierbar |
| Typischer Nutzen | Struktur und Änderbarkeit ohne verteilte Komplexität | Unabhängige Skalierung und autonome Teams |
Für viele mittelgroße Anwendungen ist der modulare Monolith der vernünftigere Startpunkt. Er hält die Laufzeitarchitektur einfach und lässt trotzdem zu, dass fachliche Grenzen später weiterentwickelt werden. Eine spätere Aufteilung in Services wird dadurch nicht garantiert, aber sie wird deutlich realistischer.
Microservices lohnen sich eher, wenn unabhängige Releases, stark unterschiedliche Skalierungsprofile oder organisatorisch getrennte Teams wirklich vorhanden sind. Nur wegen einer großen Package-Struktur mehrere Services zu bauen, erzeugt meiner Erfahrung nach oft mehr technische Arbeit als geschäftlichen Nutzen.
Wo die Grenzen liegen und welche Fehler häufig passieren
Das Framework kann keine schlechte Domänenmodellierung reparieren. Wenn ein Modul gleichzeitig Bestellungen, Benutzerverwaltung und Infrastrukturaufgaben enthält, bleibt es trotz technischer Prüfung fachlich unscharf.
Zu große gemeinsame Bereiche
Ein allgegenwärtiges common- oder shared-Package wirkt zunächst praktisch. Mit der Zeit landen dort jedoch Modelle, Hilfsservices und Datenbankzugriffe, die jedes Modul direkt verwenden darf. So entsteht eine gemeinsame interne Plattform, die Änderungen besonders riskant macht.
Events als versteckte Fernaufrufe
Ein Event sollte eine fachliche Tatsache ausdrücken, nicht bloß einen Methodenaufruf verschleiern. Namen wie RunShippingNow zeigen häufig, dass die Kommunikation zu technisch gedacht ist. Besser sind Ereignisse wie OrderConfirmed, aus denen das empfangende Modul seine eigene Reaktion ableiten kann.
Gemeinsame Datenbank als Freifahrtschein
Eine gemeinsame Datenbank ist am Anfang erlaubt und oft sinnvoll. Sie darf aber nicht dazu führen, dass jedes Modul Tabellen anderer Bereiche direkt verändert. Wer die Datenhoheit nicht klärt, hat zwar Package-Grenzen, aber keine echten fachlichen Grenzen.
Lesen Sie auch: Boilerplate-Code richtig nutzen - Vorlagen, Risiken und Generatoren
Architekturtest ohne Teamregeln
Ein automatischer Test schützt nur die Regeln, die tatsächlich definiert wurden. Zusätzlich sollten Code Reviews prüfen, ob neue Abhängigkeiten fachlich plausibel sind und ob ein direkter Aufruf wirklich notwendig ist. Technik und Teamdisziplin müssen hier zusammenspielen.
Wann sich der Einsatz besonders lohnt
Spring Modulith passt gut zu Spring-Boot-Anwendungen, die mehrere fachliche Bereiche enthalten, aber noch nicht die organisatorische oder technische Reife für eine verteilte Architektur benötigen. Besonders hilfreich ist es bei Systemen mit Bestellungen, Kundenkonten, Abrechnung, Lager oder Workflow-Prozessen.
Weniger geeignet ist der Ansatz für eine sehr kleine Anwendung mit wenigen Klassen und ohne erkennbare fachliche Teilbereiche. Auch bei einem bereits streng getrennten Multi-Repository-System bringt die Bibliothek nicht automatisch einen großen Zusatznutzen.
Ich würde die Entscheidung an drei Fragen festmachen. Gibt es fachliche Grenzen, die im Code sichtbar werden sollen? Entstehen regelmäßig unerlaubte Abhängigkeiten? Und würde ein späterer Service-Schnitt helfen, wenn die Module heute bereits sauber getrennt wären? Wenn mindestens zwei Antworten positiv ausfallen, ist ein Proof of Concept meist schnell gerechtfertigt.
Ein belastbarer Start für die nächste Ausbaustufe
Der größte Nutzen entsteht nicht durch das Hinzufügen einer weiteren Bibliothek, sondern durch die konsequente Verbindung aus fachlicher Struktur, überprüfbaren Abhängigkeiten und passenden Tests. Beginnen Sie mit wenigen Modulen, halten Sie deren öffentliche APIs klein und lassen Sie Architekturtests bei jedem Build mitlaufen.
Für die Abhängigkeiten sollten Sie die zur verwendeten Spring-Boot-Version passende Modulith-BOM nutzen und die offizielle Dokumentation von Spring regelmäßig gegenprüfen. Versionssprünge können Änderungen bei Java, Spring Boot oder der Ereignisverarbeitung mitbringen, deshalb gehört die Kompatibilität in den normalen Upgrade-Prozess.
Wenn die Grenzen stabil bleiben, kann ein modularer Monolith über Jahre hinweg eine sehr leistungsfähige Zielarchitektur sein. Er muss nicht automatisch ein Zwischenschritt zu Microservices werden, sondern kann selbst die pragmatische Lösung sein, die ein Team langfristig schneller, sicherer und planbarer arbeiten lässt.