Spring Modulith: Modulare Monolith-Architektur erklärt

Spring Modulith lernen, um modulare Monolithen in Java zu bauen. Architektur, Module, asynchrone Events, Tests und Observability mit Spring Boot 3 und 4 Codebeispielen.

Spring Modulith: modulare Monolith-Architektur mit Spring Boot

Spring Modulith bietet einen pragmatischen Ansatz, um Spring-Boot-Anwendungen in zusammenhängende fachliche Module zu strukturieren. Diese Architektur positioniert den modularen Monolithen zwischen klassischem Monolith und Microservices und bietet starke Modularität ohne die operative Komplexität verteilter Systeme.

Wichtige Erkenntnis

Spring Modulith formalisiert Best Practices der hexagonalen Architektur und des Domain-Driven Design direkt in Spring Boot und überprüft Modulabhängigkeiten automatisch.

Warum auf einen modularen Monolithen setzen?

Das Problem des klassischen Monolithen

Klassische Monolithen leiden unter zu starker Kopplung zwischen Komponenten. Mit der Zeit häufen sich kreuz- und querliegende Abhängigkeiten und verwandeln die Anwendung in einen unwartbaren "Big Ball of Mud". Eine Änderung im Abrechnungsmodul wirkt sich auf das Benutzermodul aus, dann auf das Benachrichtigungsmodul, mit unvorhersehbaren Seiteneffekten.

AntiPattern.javajava
// Direct coupling between modules - AVOID THIS
@Service
public class OrderService {

    // Direct dependencies to other modules
    // Creates tight coupling and dependency cycles
    private final UserRepository userRepository;
    private final InventoryService inventoryService;
    private final PaymentProcessor paymentProcessor;
    private final NotificationService notificationService;
    private final ShippingCalculator shippingCalculator;

    public OrderService(UserRepository userRepository,
                        InventoryService inventoryService,
                        PaymentProcessor paymentProcessor,
                        NotificationService notificationService,
                        ShippingCalculator shippingCalculator) {
        this.userRepository = userRepository;
        this.inventoryService = inventoryService;
        this.paymentProcessor = paymentProcessor;
        this.notificationService = notificationService;
        this.shippingCalculator = shippingCalculator;
    }

    public Order createOrder(OrderRequest request) {
        // This service knows too many implementation details
        User user = userRepository.findById(request.userId()).orElseThrow();
        inventoryService.reserveItems(request.items());
        BigDecimal shipping = shippingCalculator.calculate(user.getAddress());
        paymentProcessor.charge(user, request.total().add(shipping));
        notificationService.sendOrderConfirmation(user, request);
        // ...
        return null;
    }
}

Dieses Muster erzeugt konkrete Probleme: brüchige Integrationstests, Schwierigkeiten, Auswirkungen einer Änderung zu beurteilen, und die Unmöglichkeit, ein Modul unabhängig zu deployen oder weiterzuentwickeln.

Microservices sind nicht immer die Antwort

Microservices lösen das Kopplungsproblem, bringen aber erhebliche operative Komplexität mit sich: Netzwerkkommunikation, eventual consistency, verteiltes Deployment, Beobachtbarkeit über mehrere Dienste hinweg. Für viele Teams steht diese Komplexität nicht im Verhältnis zum Nutzen.

Der modulare Monolith bietet eine Alternative: eine einzige Deployment-Einheit mit klar definierten und durchgesetzten Modulgrenzen. Spring Modulith automatisiert die Überprüfung dieser Grenzen.

Erste Schritte mit Spring Modulith

Versionsauswahl und Spring Boot Kompatibilität

Spring Modulith unterhält zwei aktive Release-Zweige mit unterschiedlichen Spring-Boot-Anforderungen:

Spring ModulithSpring BootAnwendungsfall
2.1.x4.0+Neue Projekte auf Spring Boot 4
1.4.x3.1 bis 3.5Bestehende Spring Boot 3 Projekte

Für Projekte auf Spring Boot 3.x ist der 1.4-Zweig zu verwenden. Die folgenden Beispiele nutzen 1.4.12 mit Spring Boot 3.5.

Projektkonfiguration

Spring Modulith in ein Spring-Boot-Projekt zu integrieren erfordert einige Maven-Abhängigkeiten. Der Haupt-Starter aktiviert die automatische Modulerkennung.

xml
<!-- pom.xml -->
<!-- Spring Modulith dependencies for Spring Boot 3.5 -->
<dependencies>
    <!-- Core Spring Modulith -->
    <dependency>
        <groupId>org.springframework.modulith</groupId>
        <artifactId>spring-modulith-starter-core</artifactId>
    </dependency>

    <!-- Async event support with persistence -->
    <dependency>
        <groupId>org.springframework.modulith</groupId>
        <artifactId>spring-modulith-starter-jpa</artifactId>
    </dependency>

    <!-- Module structure tests -->
    <dependency>
        <groupId>org.springframework.modulith</groupId>
        <artifactId>spring-modulith-starter-test</artifactId>
        <scope>test</scope>
    </dependency>

    <!-- Automatic documentation generation -->
    <dependency>
        <groupId>org.springframework.modulith</groupId>
        <artifactId>spring-modulith-docs</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.modulith</groupId>
            <artifactId>spring-modulith-bom</artifactId>
            <version>1.4.12</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

Modulstruktur

Spring Modulith erkennt Module automatisch anhand der direkten Pakete unterhalb des Hauptanwendungspakets. Jedes Unterpaket repräsentiert ein eigenes Modul mit eigenen Zuständigkeiten.

text
com.example.shop/
├── ShopApplication.java        # Spring Boot entry point
├── order/                       # Order Module
│   ├── Order.java              # Public entity (module API)
│   ├── OrderService.java       # Public service
│   ├── internal/               # Module-internal package
│   │   ├── OrderRepository.java
│   │   └── OrderValidator.java
│   └── OrderCreatedEvent.java  # Published event
├── inventory/                   # Inventory Module
│   ├── InventoryService.java
│   ├── Product.java
│   └── internal/
│       └── StockRepository.java
├── customer/                    # Customer Module
│   ├── Customer.java
│   ├── CustomerService.java
│   └── internal/
│       └── CustomerRepository.java
└── notification/                # Notification Module
    ├── NotificationService.java
    └── internal/
        └── EmailSender.java

Diese Konvention legt eine Grundregel fest: Nur die Klassen im Wurzelpaket des Moduls (nicht in internal/) bilden die öffentliche API, die anderen Modulen zugänglich ist.

Order.javajava
// Public entity of Order module - accessible from other modules
package com.example.shop.order;

import jakarta.persistence.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.UUID;

@Entity
@Table(name = "orders")
public class Order {

    @Id
    private UUID id;

    // Reference by ID rather than entity
    // Avoids direct coupling with Customer module
    private UUID customerId;

    private BigDecimal totalAmount;

    @Enumerated(EnumType.STRING)
    private OrderStatus status;

    private LocalDateTime createdAt;

    protected Order() {
        // JPA constructor
    }

    public Order(UUID customerId, BigDecimal totalAmount) {
        this.id = UUID.randomUUID();
        this.customerId = customerId;
        this.totalAmount = totalAmount;
        this.status = OrderStatus.PENDING;
        this.createdAt = LocalDateTime.now();
    }

    // Public getters - part of module API
    public UUID getId() { return id; }
    public UUID getCustomerId() { return customerId; }
    public BigDecimal getTotalAmount() { return totalAmount; }
    public OrderStatus getStatus() { return status; }
    public LocalDateTime getCreatedAt() { return createdAt; }

    // Encapsulated business methods
    void confirm() {
        if (this.status != OrderStatus.PENDING) {
            throw new IllegalStateException("Only pending orders can be confirmed");
        }
        this.status = OrderStatus.CONFIRMED;
    }
}
OrderRepository.javajava
// Internal repository - NOT accessible from other modules
package com.example.shop.order.internal;

import com.example.shop.order.Order;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.UUID;

// This repository is in the internal package
// Spring Modulith prohibits access from other modules
interface OrderRepository extends JpaRepository<Order, UUID> {

    // Methods specific to the Order module
    List<Order> findByCustomerIdAndStatus(UUID customerId, OrderStatus status);
}

Das Repository bleibt intern, weil der Datenzugriff über den öffentlichen Service erfolgen muss, um die Kapselung der Geschäftslogik zu gewährleisten. Dieses Muster gilt für alle JPA-Entitäten und -Beziehungen in einer modularen Architektur.

Namenskonvention

Das Paket internal hat für Java keine besondere Bedeutung. Es ist eine Konvention, die Spring Modulith erkennt und während der Tests automatisch überprüft. Jeder Verstoß erzeugt einen expliziten Fehler.

Kommunikation zwischen Modulen

Domain-Events

Die Kommunikation zwischen Modulen erfolgt über Domain-Events statt direkter Aufrufe. Dieses Muster entkoppelt sendende von empfangenden Modulen.

OrderCreatedEvent.javajava
// Event published by Order module
package com.example.shop.order;

import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.UUID;

// Immutable record representing the event
// Contains only information needed by consumers
public record OrderCreatedEvent(
    UUID orderId,
    UUID customerId,
    BigDecimal totalAmount,
    LocalDateTime createdAt
) {
    // Factory method to create event from entity
    public static OrderCreatedEvent from(Order order) {
        return new OrderCreatedEvent(
            order.getId(),
            order.getCustomerId(),
            order.getTotalAmount(),
            order.getCreatedAt()
        );
    }
}
OrderService.javajava
// Public service that publishes events
package com.example.shop.order;

import com.example.shop.order.internal.OrderRepository;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.UUID;

@Service
@Transactional
public class OrderService {

    private final OrderRepository orderRepository;
    private final ApplicationEventPublisher eventPublisher;

    public OrderService(OrderRepository orderRepository,
                        ApplicationEventPublisher eventPublisher) {
        this.orderRepository = orderRepository;
        this.eventPublisher = eventPublisher;
    }

    public Order createOrder(UUID customerId, BigDecimal amount) {
        // Create the order
        Order order = new Order(customerId, amount);
        order = orderRepository.save(order);

        // Publish the event
        // Interested modules will react asynchronously
        eventPublisher.publishEvent(OrderCreatedEvent.from(order));

        return order;
    }

    public Order confirmOrder(UUID orderId) {
        Order order = orderRepository.findById(orderId)
            .orElseThrow(() -> new OrderNotFoundException(orderId));

        order.confirm();

        // Confirmation event
        eventPublisher.publishEvent(new OrderConfirmedEvent(
            order.getId(),
            order.getCustomerId()
        ));

        return order;
    }
}

Andere Module konsumieren diese Events, ohne Implementierungsdetails des Order-Moduls zu kennen.

NotificationEventListener.javajava
// Notification module consuming Order events
package com.example.shop.notification.internal;

import com.example.shop.order.OrderCreatedEvent;
import com.example.shop.order.OrderConfirmedEvent;
import org.springframework.modulith.events.ApplicationModuleListener;
import org.springframework.stereotype.Component;

@Component
class NotificationEventListener {

    private final EmailSender emailSender;
    private final CustomerLookup customerLookup;

    NotificationEventListener(EmailSender emailSender,
                               CustomerLookup customerLookup) {
        this.emailSender = emailSender;
        this.customerLookup = customerLookup;
    }

    // @ApplicationModuleListener guarantees async processing
    // and event persistence for retry on failure
    @ApplicationModuleListener
    void onOrderCreated(OrderCreatedEvent event) {
        // Retrieve email via local interface
        // Avoids direct dependency on Customer module
        String email = customerLookup.getEmailByCustomerId(event.customerId());

        emailSender.send(
            email,
            "Order Received",
            "Your order #%s has been received.".formatted(event.orderId())
        );
    }

    @ApplicationModuleListener
    void onOrderConfirmed(OrderConfirmedEvent event) {
        String email = customerLookup.getEmailByCustomerId(event.customerId());

        emailSender.send(
            email,
            "Order Confirmed",
            "Your order #%s is confirmed and being prepared."
                .formatted(event.orderId())
        );
    }
}

Bereit für deine Spring Boot-Interviews?

Übe mit unseren interaktiven Simulatoren, Flashcards und technischen Tests.

Persistierte und asynchrone Events

Spring Modulith bietet eine starke Funktion: die Persistenz von Events. Events werden vor der Veröffentlichung in der Datenbank abgelegt, was die Verarbeitung selbst bei Anwendungsabsturz garantiert.

EventPublicationConfig.javajava
// Persisted events configuration
package com.example.shop.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.modulith.events.config.EnablePersistentDomainEvents;
import org.springframework.scheduling.annotation.EnableAsync;

@Configuration
@EnableAsync
@EnablePersistentDomainEvents  // Enables event persistence
public class EventPublicationConfig {

    // Spring Modulith automatically creates required tables
    // EVENT_PUBLICATION stores pending events
    // Processed events are marked as completed
}
InventoryEventListener.javajava
// Listener with transactional event handling
package com.example.shop.inventory.internal;

import com.example.shop.order.OrderCreatedEvent;
import com.example.shop.order.OrderCancelledEvent;
import org.springframework.modulith.events.ApplicationModuleListener;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

@Component
class InventoryEventListener {

    private final StockRepository stockRepository;

    InventoryEventListener(StockRepository stockRepository) {
        this.stockRepository = stockRepository;
    }

    // Transactional processing - if exception thrown,
    // event will be retried automatically
    @ApplicationModuleListener
    @Transactional
    void onOrderCreated(OrderCreatedEvent event) {
        // Reserve stock for this order
        // On failure, event remains in EVENT_PUBLICATION
        // and will be reprocessed in next cycle
        reserveStockForOrder(event.orderId(), event.items());
    }

    @ApplicationModuleListener
    @Transactional
    void onOrderCancelled(OrderCancelledEvent event) {
        // Release reserved stock
        releaseStockForOrder(event.orderId());
    }

    private void reserveStockForOrder(UUID orderId, List<OrderItem> items) {
        for (OrderItem item : items) {
            Stock stock = stockRepository.findByProductId(item.productId())
                .orElseThrow(() -> new StockNotFoundException(item.productId()));

            stock.reserve(item.quantity());
            stockRepository.save(stock);
        }
    }

    private void releaseStockForOrder(UUID orderId) {
        // Stock release implementation
    }
}

Die von Spring Modulith automatisch erstellte Tabelle EVENT_PUBLICATION:

sql
-- EVENT_PUBLICATION table structure (PostgreSQL)
CREATE TABLE event_publication (
    id UUID PRIMARY KEY,
    listener_id VARCHAR(512) NOT NULL,
    event_type VARCHAR(512) NOT NULL,
    serialized_event TEXT NOT NULL,
    publication_date TIMESTAMP NOT NULL,
    completion_date TIMESTAMP
);

-- Index for retry queries
CREATE INDEX idx_event_publication_incomplete
ON event_publication (completion_date)
WHERE completion_date IS NULL;

Event-Externalisierung mit JobRunr (Spring Modulith 2.1+)

Spring Modulith 2.1 führt Outbox-Support für Event-Externalisierung mit JobRunr und Namastack ein. Dies ermöglicht die zuverlässige Zustellung von Events an externe Message Broker.

JobRunr outbox configuration (Spring Modulith 2.1+)java
package com.example.shop.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.modulith.events.externalize.Externalized;

@Configuration
public class EventExternalizationConfig {
    // With spring-modulith-events-jobrunr dependency,
    // events annotated with @Externalized are automatically
    // persisted as JobRunr jobs for reliable delivery
}
OrderCreatedEvent.javajava
// Event marked for external publication
package com.example.shop.order;

import org.springframework.modulith.events.externalize.Externalized;

@Externalized(target = "orders")  // Kafka topic or queue name
public record OrderCreatedEvent(
    UUID orderId,
    UUID customerId,
    BigDecimal totalAmount,
    LocalDateTime createdAt
) {
    public static OrderCreatedEvent from(Order order) {
        return new OrderCreatedEvent(
            order.getId(),
            order.getCustomerId(),
            order.getTotalAmount(),
            order.getCreatedAt()
        );
    }
}

Dieses Outbox-Muster garantiert at-least-once Delivery: Events überleben Anwendungsstarts und werden erst entfernt, nachdem die externe Publikation erfolgreich war.

Zwischen Modulen offengelegte Schnittstellen

Wenn ein Modul Informationen aus einem anderen benötigt, ohne ein Event zu nutzen, erlaubt eine öffentliche Schnittstelle im Quellmodul minimale Kopplung.

CustomerLookup.javajava
// Public interface of Customer module
package com.example.shop.customer;

import java.util.Optional;
import java.util.UUID;

// Interface exposed to other modules
// Defines contract without exposing implementation details
public interface CustomerLookup {

    Optional<String> findEmailById(UUID customerId);

    boolean exists(UUID customerId);

    // Specific DTO for shared information
    Optional<CustomerInfo> findInfoById(UUID customerId);

    record CustomerInfo(
        UUID id,
        String email,
        String fullName,
        String preferredLanguage
    ) {}
}
CustomerLookupImpl.javajava
// Internal implementation
package com.example.shop.customer.internal;

import com.example.shop.customer.CustomerLookup;
import org.springframework.stereotype.Component;
import java.util.Optional;
import java.util.UUID;

@Component
class CustomerLookupImpl implements CustomerLookup {

    private final CustomerRepository customerRepository;

    CustomerLookupImpl(CustomerRepository customerRepository) {
        this.customerRepository = customerRepository;
    }

    @Override
    public Optional<String> findEmailById(UUID customerId) {
        return customerRepository.findById(customerId)
            .map(Customer::getEmail);
    }

    @Override
    public boolean exists(UUID customerId) {
        return customerRepository.existsById(customerId);
    }

    @Override
    public Optional<CustomerInfo> findInfoById(UUID customerId) {
        return customerRepository.findById(customerId)
            .map(customer -> new CustomerInfo(
                customer.getId(),
                customer.getEmail(),
                customer.getFullName(),
                customer.getPreferredLanguage()
            ));
    }
}

Dieser Ansatz ermöglicht es dem Notification-Modul, auf Kundeninformationen zuzugreifen, ohne direkt vom Repository oder der Customer-Entität abzuhängen.

Tests zur Modulstruktur

Automatische Abhängigkeitsprüfung

Spring Modulith stellt Test-Werkzeuge bereit, um die Einhaltung der Architekturregeln zu prüfen. Diese Tests schlagen fehl, sobald ein Modul auf interne Klassen eines anderen zugreift.

ModularityTests.javajava
// Modular architecture verification tests
package com.example.shop;

import org.junit.jupiter.api.Test;
import org.springframework.modulith.core.ApplicationModules;
import org.springframework.modulith.docs.Documenter;

class ModularityTests {

    // Load application module structure
    private final ApplicationModules modules = ApplicationModules.of(ShopApplication.class);

    @Test
    void verifyModularStructure() {
        // Verify all modules are correctly structured
        // Fails if a module accesses internal packages of another
        modules.verify();
    }

    @Test
    void printModuleOverview() {
        // Print module structure to console
        // Useful for understanding dependencies
        modules.forEach(System.out::println);
    }

    @Test
    void createModuleDocumentation() {
        // Generate automatic module documentation
        // Includes dependency diagrams
        new Documenter(modules)
            .writeModulesAsPlantUml()
            .writeIndividualModulesAsPlantUml();
    }

    @Test
    void detectCyclicDependencies() {
        // The verify() method also detects cycles
        // Module A → Module B → Module C → Module A = failure
        modules.verify();
    }
}

Die Ausführung von modules.verify() analysiert den Bytecode und erkennt:

  • Zugriffe auf internal-Pakete aus anderen Modulen
  • Zyklische Abhängigkeiten zwischen Modulen
  • Verstöße gegen die Kapselungsregeln

Integrationstests pro Modul

Spring Modulith erlaubt es, jedes Modul isoliert zu testen, indem nur die nötigen Beans geladen werden. Dieser Ansatz ergänzt Testcontainers-basierte Integrationstests für die Datenbankverifikation.

OrderModuleIntegrationTests.javajava
// Order module integration test in isolation
package com.example.shop.order;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.modulith.test.ApplicationModuleTest;
import org.springframework.modulith.test.Scenario;
import java.math.BigDecimal;
import java.util.UUID;

import static org.assertj.core.api.Assertions.assertThat;

@ApplicationModuleTest  // Load only Order module and its dependencies
class OrderModuleIntegrationTests {

    @Autowired
    private OrderService orderService;

    @Test
    void shouldCreateOrder() {
        // Given
        UUID customerId = UUID.randomUUID();
        BigDecimal amount = new BigDecimal("99.99");

        // When
        Order order = orderService.createOrder(customerId, amount);

        // Then
        assertThat(order.getId()).isNotNull();
        assertThat(order.getCustomerId()).isEqualTo(customerId);
        assertThat(order.getStatus()).isEqualTo(OrderStatus.PENDING);
    }

    @Test
    void shouldPublishEventOnOrderCreation(Scenario scenario) {
        // Given
        UUID customerId = UUID.randomUUID();

        // When / Then - verify event is published
        scenario.stimulate(() -> orderService.createOrder(customerId, BigDecimal.TEN))
            .andWaitForEventOfType(OrderCreatedEvent.class)
            .matching(event -> event.customerId().equals(customerId))
            .toArriveAndVerify(event -> {
                assertThat(event.orderId()).isNotNull();
                assertThat(event.totalAmount()).isEqualTo(BigDecimal.TEN);
            });
    }

    @Test
    void shouldHandleOrderConfirmation(Scenario scenario) {
        // Given - create an order
        Order order = orderService.createOrder(UUID.randomUUID(), BigDecimal.TEN);

        // When / Then - confirm and verify event
        scenario.stimulate(() -> orderService.confirmOrder(order.getId()))
            .andWaitForEventOfType(OrderConfirmedEvent.class)
            .toArriveAndVerify(event -> {
                assertThat(event.orderId()).isEqualTo(order.getId());
            });
    }
}

Die Annotation @ApplicationModuleTest konfiguriert automatisch:

  • Das Laden ausschließlich der Beans des Order-Moduls
  • Mocks für Abhängigkeiten zu anderen Modulen
  • Die Test-Infrastruktur für Events

Slice Test Support (Spring Modulith 2.1+)

Spring Modulith 2.1 integriert den Slice Test Support von Spring Boot. Modultests lassen sich nun mit @DataJpaTest oder @WebMvcTest für gezieltes Testen kombinieren.

OrderRepositorySliceTest.javajava
// Combining module isolation with slice testing
package com.example.shop.order;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.modulith.test.ApplicationModuleTest;
import org.springframework.modulith.test.ApplicationModuleTest.BootstrapMode;

@ApplicationModuleTest(BootstrapMode.STANDALONE)
@DataJpaTest  // Only load JPA infrastructure
class OrderRepositorySliceTest {

    @Autowired
    private OrderRepository orderRepository;

    @Test
    void shouldPersistAndRetrieveOrder() {
        // Test runs with minimal context
        // Only JPA beans from Order module are loaded
    }
}
OrderNotificationIntegrationTest.javajava
// Inter-module integration test
package com.example.shop;

import com.example.shop.order.OrderService;
import com.example.shop.order.OrderCreatedEvent;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.modulith.test.ApplicationModuleTest;
import org.springframework.modulith.test.Scenario;
import org.springframework.modulith.test.ApplicationModuleTest.BootstrapMode;
import java.math.BigDecimal;
import java.util.UUID;

// DIRECT loads all directly dependent modules
@ApplicationModuleTest(BootstrapMode.DIRECT)
class OrderNotificationIntegrationTest {

    @Autowired
    private OrderService orderService;

    @Test
    void shouldTriggerNotificationOnOrderCreated(Scenario scenario) {
        // This test verifies Order → Notification integration
        UUID customerId = UUID.randomUUID();

        scenario.stimulate(() -> orderService.createOrder(customerId, BigDecimal.TEN))
            .andWaitForEventOfType(OrderCreatedEvent.class)
            .toArriveAndVerify(event -> {
                // Event was processed by NotificationEventListener
                // Test verifies email was sent
            });
    }
}
Test-Isolation

Verwende BootstrapMode.STANDALONE (Standard) für Modul-Unit-Tests. Behalte BootstrapMode.ALL_DEPENDENCIES den End-to-End-Integrationstests vor, um versteckte Abhängigkeiten zu vermeiden.

Erweiterte Modulkonfiguration

Explizite Module mit @ApplicationModule

Für komplexe Fälle erlaubt die Annotation @ApplicationModule die explizite Konfiguration der Modulregeln.

package-info.javajava
// Explicit Order module configuration
@org.springframework.modulith.ApplicationModule(
    // Modules allowed to depend on this one
    allowedDependencies = {"customer", "inventory"},
    // Module type: OPEN (free access) or CLOSED (explicit API)
    type = Type.CLOSED
)
package com.example.shop.order;

import org.springframework.modulith.ApplicationModule.Type;
NamedInterface.javajava
// Named interface definition for finer API control
package com.example.shop.order;

import org.springframework.modulith.NamedInterface;

// Exposes only certain classes as public API
@NamedInterface("order-api")
public class OrderApi {
    // Classes in this package are accessible via "order-api"
}
package-info.javajava
// Module depending on a specific named interface
@org.springframework.modulith.ApplicationModule(
    allowedDependencies = "order::order-api"  // Access limited to named API
)
package com.example.shop.shipping;

Umgang mit zirkulären Abhängigkeiten

Zirkuläre Abhängigkeiten zwischen Modulen weisen meist auf ein Designproblem hin. Spring Modulith erkennt sie und lässt die Verifizierung scheitern. Die Lösung besteht in der Regel darin, ein neues Modul auszulagern oder Events zu nutzen.

java
// BEFORE - Circular dependency
// Order → Inventory (to check stock)
// Inventory → Order (to know current orders)

// AFTER - Resolution through events
// Order publishes OrderCreatedEvent
// Inventory listens and reserves stock
// Inventory publishes StockReservedEvent
// Order listens and confirms availability
StockReservedEvent.javajava
// Event published by Inventory
package com.example.shop.inventory;

import java.util.UUID;

public record StockReservedEvent(
    UUID orderId,
    UUID productId,
    int quantity,
    boolean success,
    String failureReason
) {}
OrderStockListener.javajava
// Order module listens to Inventory events
package com.example.shop.order.internal;

import com.example.shop.inventory.StockReservedEvent;
import org.springframework.modulith.events.ApplicationModuleListener;
import org.springframework.stereotype.Component;

@Component
class OrderStockListener {

    private final OrderRepository orderRepository;

    OrderStockListener(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }

    @ApplicationModuleListener
    void onStockReserved(StockReservedEvent event) {
        Order order = orderRepository.findById(event.orderId())
            .orElseThrow();

        if (event.success()) {
            order.markStockReserved();
        } else {
            order.markStockUnavailable(event.failureReason());
        }

        orderRepository.save(order);
    }
}

Beobachtbarkeit und Monitoring

Event-Tracing

Spring Modulith integriert sich mit Micrometer für verteiltes Tracing von Events zwischen Modulen. Der spring-modulith-starter-insight-Starter bündelt die Observability-Abhängigkeiten.

xml
<!-- pom.xml -->
<!-- Observability starter (Spring Modulith 1.4+) -->
<dependency>
    <groupId>org.springframework.modulith</groupId>
    <artifactId>spring-modulith-starter-insight</artifactId>
    <scope>runtime</scope>
</dependency>
ObservabilityConfig.javajava
// Module observability configuration
package com.example.shop.config;

import io.micrometer.observation.ObservationRegistry;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.modulith.observability.ModuleEventListener;

@Configuration
public class ObservabilityConfig {

    @Bean
    ModuleEventListener moduleEventListener(ObservationRegistry registry) {
        // Adds spans for each processed event
        return new ModuleEventListener(registry);
    }
}
yaml
# application.yml
# Observability configuration
management:
  tracing:
    sampling:
      probability: 1.0  # Trace all events in dev
  endpoints:
    web:
      exposure:
        include: health,info,metrics,modulith

spring:
  modulith:
    events:
      # Retry interval for failed events
      republish-outstanding-events-on-restart: true
      # Retention duration for completed events
      completion-mode: DELETE  # or ARCHIVE

Modul-Metriken (Spring Modulith 1.4+)

Spring Modulith 1.4 führt automatische Event-Zähler über die ModulithEventMetrics-API ein. Zähler erfassen Events, die von jedem Modul veröffentlicht werden.

MetricsCustomization.javajava
// Custom event metrics
package com.example.shop.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.modulith.observability.ModulithEventMetricsCustomizer;

@Configuration
public class MetricsCustomization {

    @Bean
    ModulithEventMetricsCustomizer metricsCustomizer() {
        return metrics -> {
            // Add custom tags or configure metric behavior
            // Metrics automatically include module name and event type
        };
    }
}

Modul-Actuator-Endpoint

Spring Modulith stellt einen Actuator-Endpoint bereit, um den Modulzustand in Produktion sichtbar zu machen. Für detaillierte Metriken und Observability siehe Spring Boot Actuator Monitoring.

ModulithActuatorConfig.javajava
// Actuator endpoint activation
package com.example.shop.config;

import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.modulith.actuator.ApplicationModulesEndpoint;
import org.springframework.modulith.core.ApplicationModules;

@Configuration
public class ModulithActuatorConfig {

    @Bean
    @ConditionalOnAvailableEndpoint
    ApplicationModulesEndpoint modulesEndpoint(ApplicationModules modules) {
        return new ApplicationModulesEndpoint(modules);
    }
}

Der Endpoint /actuator/modulith liefert:

json
{
  "modules": [
    {
      "name": "order",
      "basePackage": "com.example.shop.order",
      "dependencies": ["customer"],
      "publishedEvents": [
        "com.example.shop.order.OrderCreatedEvent",
        "com.example.shop.order.OrderConfirmedEvent"
      ],
      "listenedEvents": [
        "com.example.shop.inventory.StockReservedEvent"
      ]
    },
    {
      "name": "inventory",
      "basePackage": "com.example.shop.inventory",
      "dependencies": [],
      "publishedEvents": [
        "com.example.shop.inventory.StockReservedEvent"
      ],
      "listenedEvents": [
        "com.example.shop.order.OrderCreatedEvent"
      ]
    }
  ]
}

Migration zu Microservices

Vorbereitung der Extraktion

Die modulare Architektur erleichtert eine spätere Extraktion zu Microservices. Jedes Modul wird zu einem natürlichen Kandidaten für die Extraktion.

ExtractionReadinessChecker.javajava
// Extraction readiness verification
package com.example.shop;

import org.springframework.modulith.core.ApplicationModules;
import org.springframework.modulith.core.ApplicationModule;

public class ExtractionReadinessChecker {

    public void checkModule(String moduleName) {
        ApplicationModules modules = ApplicationModules.of(ShopApplication.class);
        ApplicationModule module = modules.getModuleByName(moduleName)
            .orElseThrow();

        System.out.println("Module: " + moduleName);
        System.out.println("Dependencies: " + module.getDependencies());
        System.out.println("Published Events: " + module.getPublishedEvents());
        System.out.println("Listened Events: " + module.getBootstrapDependencies());

        // A module ready for extraction:
        // - Communicates only through events
        // - Has no synchronous dependencies to other modules
        // - Owns its own data tables
    }
}

Module, die ausschließlich über Events kommunizieren, lassen sich mit minimalen Anpassungen zu Microservices extrahieren: Es genügt, den lokalen Event-Bus durch einen Message-Broker (Kafka, RabbitMQ) zu ersetzen.

Quellen

Vorteile der Spring Modulith Architektur

Spring Modulith liefert eine pragmatische Lösung, um monolithische Spring-Boot-Anwendungen zu strukturieren:

  • Strukturierung per Konvention: Pakete werden zu Modulen, internal erzwingt Kapselung
  • Entkoppelte Kommunikation: Domain-Events zwischen Modulen
  • Automatische Prüfung: Strukturtests erkennen Verstöße
  • Persistierte Events: Verarbeitungsgarantie mit @ApplicationModuleListener
  • Event-Externalisierung (2.1+): Outbox-Muster mit JobRunr für zuverlässige Zustellung
  • Isolierte Tests: @ApplicationModuleTest für jedes Modul
  • Slice Test Support (2.1+): Modulisolierung mit Spring Boot Test Slices kombinieren
  • Generierte Dokumentation: automatische PlantUML-Diagramme
  • Beobachtbarkeit: Micrometer-Integration, Event-Metriken und Actuator-Endpoint
  • Weg zu Microservices: Extraktion durch Entkopplung erleichtert

Diese Architektur passt besonders zu Teams, die ihren Monolithen strukturieren möchten, ohne die operative Komplexität von Microservices in Kauf zu nehmen, und sich dennoch die Option auf eine spätere verteilte Architektur offen halten wollen. Für häufige Interview-Fragen zur Spring Boot Architektur demonstriert das Verständnis modularer Designs Senior-Level-Denken.

Fang an zu üben!

Teste dein Wissen mit unseren Interview-Simulatoren und technischen Tests.

Tägliche Challenge

Findest du den Bug in Spring Boot?

Ein echter Codeausschnitt, ein versteckter Bug, ein Versuch pro Tag. Zum Ausprobieren ohne Konto.

Anthony Fillion-Maillet

Geschrieben von

Anthony Fillion-Maillet

Gründer von SharpSkill

Seit über 10 Jahren Fullstack-Entwickler. Er leitet SharpSkill und verantwortet alles, was hier erscheint.

Aktualisiert am 21. August 2026

Tags

#spring modulith
#modular architecture
#spring boot
#java
#modular monolith

Teilen

Verwandte Artikel