Lab 10: Spring Modulith - Modularen Monolithen strukturieren¶
Euer Immobilien-CRM hat bereits zwei Bounded Contexts (Acquisition und Brokerage),
die über ContractSigned-Events kommunizieren. Bisher nutzt ihr
Springs ApplicationEventPublisher mit @EventListener direkt.
In diesem Lab strukturiert ihr den Monolithen mit Spring Modulith, damit:
- Modul-Grenzen automatisch verifiziert werden
- Modul-Interaktionen mit der Scenario-API getestet werden
- Die Modulstruktur als Dokumentation generiert wird
- Die Event Publication Registry für At-Least-Once Delivery vorbereitet ist
Aufgabe¶
Schritt 1: Spring-Modulith-Dependencies hinzufügen¶
Erweitere die pom.xml um Spring Modulith:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.modulith</groupId>
<artifactId>spring-modulith-bom</artifactId>
<version>2.0.3</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- Existing dependencies ... -->
<!-- Spring Modulith -->
<dependency>
<groupId>org.springframework.modulith</groupId>
<artifactId>spring-modulith-starter-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.modulith</groupId>
<artifactId>spring-modulith-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Event Publication Registry (At-Least-Once Delivery) -->
<dependency>
<groupId>org.springframework.modulith</groupId>
<artifactId>spring-modulith-events-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.modulith</groupId>
<artifactId>spring-modulith-events-jackson</artifactId>
</dependency>
</dependencies>
Schritt 2: Modul-Verifikationstest schreiben und scheitern lassen¶
Erstelle die Testklasse ModulithStructureTest im Package de.realestate unter src/test/java:
class ModulithStructureTest {
ApplicationModules modules =
ApplicationModules.of(RealEstateCrmApplication.class);
@Test
void verifyModuleStructure() {
modules.verify();
}
@Test
void documentModuleStructure() {
new Documenter(modules)
.writeDocumentation();
}
}
Führe den Test aus. Er wird fehlschlagen, weil der ContractSignedListener im
Brokerage-Modul auf das interne Package de.realestate.acquisition.domain.event
zugreift. Spring Modulith betrachtet alle Subpackages als modulintern.
Schritt 3: Event in die öffentliche Modul-API verschieben¶
Verschiebe ContractSigned von de.realestate.acquisition.domain.event
nach de.realestate.acquisition (dem Modul-Root-Package):
package de.realestate.acquisition;
public record ContractSigned(
UUID contractId,
UUID propertyId,
LocalDateTime closedAt
) {}
Passe alle Imports an:
- CloseContractUseCase
- ContractSignedListener
Lösche die alte Datei de.realestate.acquisition.domain.event.ContractSigned.
Führe den Verifikationstest erneut aus - er sollte jetzt grün sein.
Schritt 4: @ApplicationModule mit erlaubten Abhängigkeiten¶
Erstelle package-info.java für jedes Modul:
de/realestate/acquisition/package-info.java:
@ApplicationModule
package de.realestate.acquisition;
import org.springframework.modulith.ApplicationModule;
de/realestate/brokerage/package-info.java:
@ApplicationModule(allowedDependencies = {"acquisition"})
package de.realestate.brokerage;
import org.springframework.modulith.ApplicationModule;
de/realestate/property/package-info.java:
@ApplicationModule
package de.realestate.property;
import org.springframework.modulith.ApplicationModule;
Schritt 5: Event Publication Registry konfigurieren¶
Erweitere die application.yml, damit die Event Publication Registry
ihre Tabelle automatisch anlegt:
spring:
modulith:
events:
jdbc:
schema-initialization:
enabled: true
Damit werden Events in einer DB-Tabelle persistiert. In Kombination mit
@TransactionalEventListener ermöglicht dies At-Least-Once Delivery -
fehlgeschlagene Event-Verarbeitungen werden beim Neustart automatisch wiederholt.
Schritt 6: @ApplicationModuleTest mit Scenario-API¶
Erstelle einen Modulith-Integrationstest für das Brokerage-Modul:
@ApplicationModuleTest
class BrokerageModuleTest {
@Autowired
private BrokerageProcessRepository repository;
@Test
void shouldCreateBrokerageProcessOnContractSigned(Scenario scenario) {
UUID contractId = UUID.randomUUID();
UUID propertyId = UUID.randomUUID();
scenario.publish(new ContractSigned(
contractId, propertyId, LocalDateTime.now()))
.andWaitForStateChange(
() -> repository.findAll().stream()
.filter(p -> p.getPropertyId().equals(propertyId))
.findFirst()
.orElse(null),
Objects::nonNull)
.andVerify(process ->
assertThat(process.getPropertyId()).isEqualTo(propertyId));
}
}
Dieser Test:
- Startet nur das Brokerage-Modul (nicht die ganze Anwendung)
- Publiziert ein Event und wartet auf den erwarteten Zustandswechsel
- Kein Thread.sleep() nötig
Bonus: Dokumentation generieren¶
Führe den documentModuleStructure()-Test aus und prüfe die generierten
PlantUML-Diagramme in target/spring-modulith-docs/.
Bonus: @TransactionalEventListener¶
Ersetze @EventListener im ContractSignedListener durch
@TransactionalEventListener(phase = AFTER_COMMIT), damit das Event erst nach
erfolgreichem Commit verarbeitet wird. Die Event Publication Registry stellt
dann sicher, dass fehlgeschlagene Verarbeitungen beim Neustart wiederholt werden.
Hinweis: Beim Umstieg auf @TransactionalEventListener muss der
ContextIntegrationTest als Verifikation dienen (@SpringBootTest mit
vollem Transaktionskontext), da Scenario.publish() kein Transaktions-Commit
simuliert.
Tipps¶
- Spring Modulith erkennt Top-Level-Packages unter der
@SpringBootApplication-Klasse automatisch als Module. Subpackages (wiedomain/,application/,adapter/) gelten als modulintern. - Events gehören zur öffentlichen API eines Moduls und müssen daher im Root-Package des Moduls liegen.
@ApplicationModule(allowedDependencies = {...})deklariert explizit, welche Module referenziert werden dürfen. Ohne diese Angabe sind alle erlaubt.- ArchUnit und Spring Modulith ergänzen sich: ArchUnit prüft feine Schicht-Regeln innerhalb eines Moduls, Spring Modulith prüft die Grenzen zwischen Modulen.
- Die Event Publication Registry nutzt die bestehende Datenquelle (H2) und erstellt automatisch die benötigte Tabelle.