Spring Modulith richtig einsetzen und Grenzen sauber ziehen

15. August 2026

Frische grüne Blätter im Sonnenlicht, symbolisch für das Erwachen im Frühling. Konferenz by CX&.

Inhaltsverzeichnis

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.

  1. Geschäftsbereiche bestimmen, etwa Bestellung, Kunde, Rechnung und Versand.
  2. Packages nach diesen Bereichen strukturieren und interne Komponenten zunächst innerhalb des Moduls halten.
  3. Öffentliche Schnittstellen definieren, statt beliebige Klassen aus anderen Modulen zu importieren.
  4. Modulgrenzen mit ApplicationModules.verify() in einem Test festschreiben.
  5. Direkte Querverbindungen reduzieren und geeignete fachliche Events einführen.
  6. 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.

Häufig gestellte Fragen

Spring Modulith erkennt Anwendungsmodule standardmäßig anhand der Package-Struktur unterhalb des Hauptpackages. Mit ApplicationModules.of(Application.class).verify() lassen sich unerlaubte Zugriffe auf interne Packages und problematische Modulzyklen als Test prüfen. Dieser Architekturtest sollte früh eingerichtet und in die Continuous Integration aufgenommen werden.

Application Events reduzieren die strukturelle Kopplung, weil ein Modul nur ein fachliches Ereignis wie OrderPlaced veröffentlicht und nicht den konkreten Service eines anderen Moduls aufrufen muss. Innerhalb eines Prozesses reicht der Spring-Event-Mechanismus häufig aus. Das empfangende Modul sollte erst reagieren, wenn die zugrunde liegende Transaktion erfolgreich abgeschlossen wurde; für Kommunikation über Prozessgrenzen hinweg können Persistenz, Wiederholungen oder ein externer Broker erforderlich sein.

Ein modularer Monolith eignet sich, wenn klare fachliche Grenzen benötigt werden, aber ein gemeinsames Deployment, ein gemeinsamer Prozess und eine gemeinsame oder logisch getrennte Datenbank ausreichen. Er verursacht weniger Aufwand für Monitoring, Deployment und Fehleranalyse. Microservices sind eher sinnvoll, wenn unabhängige Releases, stark unterschiedliche Skalierungsprofile oder organisatorisch getrennte Teams tatsächlich vorhanden sind.

Beginnen Sie mit einem fachlich klaren Bereich, etwa dem Zahlungsprozess, und strukturieren Sie Packages nach Geschäftsbereichen statt nach technischen Kategorien wie controller oder repository. Definieren Sie kleine öffentliche Schnittstellen, sichern Sie die Grenzen mit ApplicationModules.verify() und reduzieren Sie direkte Querverbindungen. Mit @ApplicationModuleTest und der Scenario-API können einzelne Module und asynchrone Abläufe gezielt getestet werden.

Artikel bewerten

Bewertung: 0.00 Stimmenanzahl: 0

Tags:

spring modulith modularer monolith application events architekturtests microservices

Beitrag teilen

Artur Lemke

Artur Lemke

Mein Name ist Artur Lemke und seit nunmehr 11 Jahren beschäftige ich mich intensiv mit der Welt der Webentwicklung, der digitalen Strategie und künstlichen Intelligenz. Diese Themen faszinieren mich, weil sie die Art und Weise, wie wir arbeiten und interagieren, grundlegend verändern. Ich liebe es, komplexe Zusammenhänge zu durchdringen und sie so aufzubereiten, dass sie für jeden verständlich werden. Hier auf metawebart.de teile ich mein Wissen, analysiere aktuelle Trends und helfe Ihnen dabei, die Potenziale von KI und digitalen Strategien für Ihr eigenes Vorhaben zu erkennen und zu nutzen. Dabei lege ich großen Wert darauf, fundierte und praxisnahe Informationen zu liefern, die Ihnen wirklich weiterhelfen.

Kommentar schreiben