# Spring Modulith Spring Modulith is a framework that enables developers to build well-structured Spring Boot applications by providing explicit support for application modules. It helps identify and enforce modular boundaries within a single deployment unit while offering verification, testing, documentation, and observability capabilities at the module level. The framework guides developers in organizing their code around domain-driven modules, where each direct sub-package of the main application package represents an independent module. The core value of Spring Modulith lies in its ability to verify architectural constraints, support integration testing of isolated modules, externalize domain events to messaging infrastructure, and generate comprehensive documentation including PlantUML component diagrams. It integrates seamlessly with Spring Boot's auto-configuration and supports various persistence backends (JDBC, JPA, MongoDB, Neo4j) for its event publication registry, ensuring reliable event delivery even across application restarts. ## @Modulithic - Application Configuration The `@Modulithic` annotation marks a Spring Boot application class to follow modulith conventions. It allows customization of module detection, shared modules, and additional base packages for multi-module setups. ```java package com.example; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.modulith.Modulithic; @Modulithic( systemName = "E-Commerce Platform", sharedModules = { "common", "security" }, additionalPackages = { "com.example.legacy" }, useFullyQualifiedModuleNames = false ) @SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } } ``` ## @ApplicationModule - Module Customization The `@ApplicationModule` annotation customizes individual module metadata including display names, allowed dependencies, and module type (open or closed). Place it on a `package-info.java` file in the module's root package. ```java // file: src/main/java/com/example/order/package-info.java @ApplicationModule( id = "order", displayName = "Order Management", allowedDependencies = { "inventory", "payment::api" }, type = ApplicationModule.Type.CLOSED ) package com.example.order; import org.springframework.modulith.ApplicationModule; ``` ## @NamedInterface - API Surface Definition The `@NamedInterface` annotation defines explicit API boundaries within a module. It marks packages or types as part of a named interface that other modules can depend on, enabling fine-grained access control. ```java // file: src/main/java/com/example/order/api/package-info.java @NamedInterface("api") package com.example.order.api; import org.springframework.modulith.NamedInterface; // Types in this package can be referenced by other modules // using: allowedDependencies = { "order::api" } ``` ```java // Alternatively, annotate individual types @NamedInterface("events") public record OrderCompleted(UUID orderId, Instant completedAt) {} ``` ## ApplicationModules - Verification and Analysis The `ApplicationModules` class provides the core API for analyzing, verifying, and documenting the module structure. It detects modules, validates dependency rules, and checks for architectural violations. ```java package com.example; import org.junit.jupiter.api.Test; import org.springframework.modulith.core.ApplicationModules; import org.springframework.modulith.docs.Documenter; class ModuleStructureTests { @Test void verifyModularStructure() { // Create module model from main application class ApplicationModules modules = ApplicationModules.of(Application.class); // Verify structure - throws exception if violations detected modules.verify(); // Access individual modules modules.getModuleByName("order") .ifPresent(module -> { System.out.println("Module: " + module.getDisplayName()); System.out.println("Base package: " + module.getBasePackage()); module.getSpringBeans().forEach(bean -> System.out.println(" Bean: " + bean.getFullyQualifiedTypeName())); }); // Iterate all modules modules.forEach(module -> System.out.println(module.getIdentifier() + ": " + module.getDirectDependencies(modules))); } @Test void generateDocumentation() { ApplicationModules modules = ApplicationModules.of(Application.class).verify(); new Documenter(modules) .writeModulesAsPlantUml() // Overview diagram .writeIndividualModulesAsPlantUml() // Per-module diagrams .writeModuleCanvases(); // Module canvas documentation } } ``` ## @ApplicationModuleTest - Module Integration Testing The `@ApplicationModuleTest` annotation bootstraps only the annotated module and its dependencies for focused integration testing. It automatically configures component scanning and entity scanning to the module scope. ```java package com.example.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.ApplicationModuleTest.BootstrapMode; @ApplicationModuleTest( mode = BootstrapMode.DIRECT_DEPENDENCIES, // Include direct deps extraIncludes = { "testutils" }, // Additional modules verifyAutomatically = true // Verify on startup ) class OrderModuleIntegrationTests { @Autowired private OrderManagement orders; @Autowired private OrderRepository repository; @Test void completesOrder() { Order order = new Order(UUID.randomUUID()); orders.complete(order); assertThat(repository.findById(order.getId())) .isPresent() .hasValueSatisfying(o -> assertThat(o.getStatus()).isEqualTo(OrderStatus.COMPLETED)); } } ``` ## Scenario - Event-Driven Testing DSL The `Scenario` class provides a fluent DSL for testing asynchronous, event-driven interactions. It supports waiting for events, state changes, and provides comprehensive assertion capabilities. ```java package com.example.order; import org.junit.jupiter.api.Test; import org.springframework.modulith.test.ApplicationModuleTest; import org.springframework.modulith.test.Scenario; import java.time.Duration; @ApplicationModuleTest class OrderEventTests { @Autowired private OrderManagement orders; @Test void publishesOrderCompletionEvent(Scenario scenario) { Order order = new Order(UUID.randomUUID()); scenario.stimulate(() -> orders.complete(order)) .andWaitForEventOfType(OrderCompleted.class) .matching(event -> event.orderId().equals(order.getId())) .toArriveAndVerify(event -> { assertThat(event.completedAt()).isNotNull(); assertThat(event.orderId()).isEqualTo(order.getId()); }); } @Test void updatesInventoryOnOrderCompletion(Scenario scenario) { Order order = new Order(UUID.randomUUID(), "PROD-001", 5); scenario.publish(new OrderCompleted(order.getId(), Instant.now())) .andWaitForStateChange(() -> inventory.getStock("PROD-001")) .andVerify(stock -> assertThat(stock).isLessThan(100)); } @Test void handlesCustomTimeout(Scenario scenario) { scenario.stimulate(() -> slowService.process()) .andWaitAtMost(Duration.ofSeconds(10)) .forEventOfType(ProcessingCompleted.class) .toArrive(); } @Test void chainsMultipleExpectations(Scenario scenario) { scenario.stimulate(() -> orders.submit(new Order())) .andWaitForStateChange(() -> orders.findPending()) .andExpect(OrderAccepted.class) .matching(e -> e.status() == Status.ACCEPTED) .toArrive(); } } ``` ## PublishedEvents - Event Assertion The `PublishedEvents` interface captures and asserts on Spring application events published during test execution. It integrates with AssertJ for fluent assertions. ```java package com.example.order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.modulith.test.AssertablePublishedEvents; import org.springframework.modulith.test.PublishedEvents; import org.springframework.modulith.test.PublishedEventsExtension; @ExtendWith(PublishedEventsExtension.class) @SpringBootTest class OrderEventAssertionTests { @Autowired private OrderManagement orders; @Test void capturesPublishedEvents(AssertablePublishedEvents events) { Order order = orders.create(new CreateOrderRequest("customer-1")); // Fluent assertions on captured events assertThat(events) .contains(OrderCreated.class) .matching(e -> e.customerId().equals("customer-1")); // Filter and verify specific events var orderEvents = events.ofType(OrderCreated.class) .matching(e -> e.orderId().equals(order.getId())); assertThat(orderEvents).hasSize(1); // Verify event properties orderEvents.forEach(event -> { assertThat(event.createdAt()).isBeforeOrEqualTo(Instant.now()); }); } @Test void assertsEventSequence(PublishedEvents events) { orders.processOrder(orderId); // Check multiple event types assertThat(events.eventOfTypeWasPublished(OrderValidated.class)).isTrue(); assertThat(events.eventOfTypeWasPublished(PaymentProcessed.class)).isTrue(); assertThat(events.eventOfTypeWasPublished(OrderShipped.class)).isTrue(); } } ``` ## @ApplicationModuleListener - Async Event Handling The `@ApplicationModuleListener` annotation combines `@Async`, `@Transactional`, and `@TransactionalEventListener` for recommended event-driven module integration. It ensures events are processed after the original transaction commits. ```java package com.example.inventory; import org.springframework.modulith.events.ApplicationModuleListener; import org.springframework.stereotype.Service; @Service public class InventoryManagement { private final InventoryRepository inventory; @ApplicationModuleListener void onOrderCompleted(OrderCompleted event) { // Runs asynchronously in a new transaction // after the order transaction has committed inventory.decreaseStock(event.productId(), event.quantity()); log.info("Inventory updated for order {}", event.orderId()); } @ApplicationModuleListener( readOnlyTransaction = true, condition = "#event.priority == 'HIGH'" ) void onHighPriorityOrder(OrderCreated event) { // Conditional processing with read-only transaction notificationService.alertWarehouse(event); } @ApplicationModuleListener(id = "inventory.shipping") void onShipmentRequested(ShipmentRequested event) { // Custom listener ID for tracking prepareShipment(event); } } ``` ## @Externalized - Event Externalization The `@Externalized` annotation marks domain events for publication to external messaging infrastructure (Kafka, RabbitMQ, JMS, etc.). Combined with the Event Publication Registry, it ensures reliable delivery. ```java package com.example.order; import org.springframework.modulith.events.Externalized; // Simple externalization - uses type name as target @Externalized public record OrderCompleted(UUID orderId, Instant completedAt) {} // Custom routing target @Externalized("orders.completed") public record OrderShipped(UUID orderId, String trackingNumber) {} // With key for partitioning (Kafka) @Externalized("orders::#{#this.customerId}") public record OrderCreated(UUID orderId, String customerId) {} ``` ## EventExternalizationConfiguration - Programmatic Externalization The `EventExternalizationConfiguration` interface provides programmatic control over which events are externalized and how they are routed to messaging targets. ```java package com.example.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.modulith.events.EventExternalizationConfiguration; import org.springframework.modulith.events.RoutingTarget; @Configuration class EventExternalizationConfig { @Bean EventExternalizationConfiguration eventExternalization() { return EventExternalizationConfiguration.externalizing() // Select events by package .selectByPackage("com.example.order.events") // Custom routing logic .routeAll(event -> { if (event instanceof OrderCompleted oc) { return RoutingTarget.forTarget("orders.completed") .withKey(oc.orderId().toString()); } return RoutingTarget.forTarget("events." + event.getClass().getSimpleName().toLowerCase()); }) // Event transformation before sending .mapping(OrderCompleted.class, event -> new OrderCompletedDTO( event.orderId().toString(), event.completedAt().toString() )) // Add custom headers .headers(OrderCompleted.class, event -> Map.of( "correlationId", event.orderId().toString(), "eventType", "ORDER_COMPLETED" )) .build(); } @Bean EventExternalizationConfiguration annotationBasedConfig() { // Default configuration using @Externalized annotations return EventExternalizationConfiguration.defaults("com.example"); } } ``` ## Event Publication Registry APIs Spring Modulith provides interfaces for managing incomplete, completed, and failed event publications. These support retry mechanisms and cleanup operations. ```java package com.example.maintenance; import org.springframework.modulith.events.*; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import java.time.Duration; @Component public class EventPublicationMaintenance { private final IncompleteEventPublications incomplete; private final CompletedEventPublications completed; private final FailedEventPublications failed; // Retry incomplete publications older than 1 hour @Scheduled(fixedRate = 60000) void resubmitIncomplete() { incomplete.resubmitIncompletePublicationsOlderThan(Duration.ofHours(1)); } // Retry with custom filter @Scheduled(fixedRate = 300000) void resubmitHighPriority() { incomplete.resubmitIncompletePublications(publication -> publication.getEvent() instanceof HighPriorityEvent); } // Retry failed publications with options @Scheduled(fixedRate = 600000) void retryFailed() { failed.resubmit(ResubmissionOptions.defaults() .maxAttempts(3) .delay(Duration.ofMinutes(5))); } // Cleanup old completed publications @Scheduled(cron = "0 0 2 * * *") void cleanupCompleted() { completed.deletePublicationsOlderThan(Duration.ofDays(7)); // Or with custom filter completed.deletePublications(publication -> publication.getEvent() instanceof TransientEvent); } } ``` ## Documenter - Documentation Generation The `Documenter` class generates PlantUML diagrams and AsciiDoc documentation for visualizing and describing module structures, dependencies, and APIs. ```java package com.example; import org.junit.jupiter.api.Test; import org.springframework.modulith.core.ApplicationModules; import org.springframework.modulith.docs.Documenter; import org.springframework.modulith.docs.Documenter.*; class DocumentationGenerator { @Test void generateDocs() { ApplicationModules modules = ApplicationModules.of(Application.class); new Documenter(modules, Options.defaults() .withOutputFolder("target/modulith-docs")) // Component diagram for all modules .writeModulesAsPlantUml(DiagramOptions.defaults() .withStyle(DiagramOptions.DiagramStyle.C4) .withDependencyDepth(DependencyDepth.ALL) .withColorSelector(module -> module.getIdentifier().toString().contains("core") ? Optional.of("#lightblue") : Optional.empty())) // Individual module diagrams .writeIndividualModulesAsPlantUml(DiagramOptions.defaults() .withTargetFileName("module-%s-dependencies.puml")) // Module canvases (detailed documentation) .writeModuleCanvases(CanvasOptions.defaults() .withApiBase("https://docs.example.com/api/") .revealInternals() .groupingBy("Services", bean -> bean.getType().isAnnotatedWith(Service.class)) .groupingBy("Repositories", bean -> bean.getType().isAssignableTo(Repository.class))) // Aggregating document .writeAggregatingDocument() // Module metadata for runtime .writeModuleMetadata(); } } ``` ## ApplicationModulesEndpoint - Actuator Integration Spring Modulith provides an actuator endpoint that exposes module structure information at runtime, useful for monitoring and operational insights. ```yaml # application.yml management: endpoints: web: exposure: include: health,info,modulith endpoint: modulith: enabled: true ``` ```bash # Access module structure via actuator curl http://localhost:8080/actuator/modulith # Response includes module names, dependencies, Spring beans { "modules": [ { "name": "order", "basePackage": "com.example.order", "dependencies": ["inventory", "payment"], "beans": [ "orderManagement", "orderRepository" ] } ] } ``` ## ApplicationModuleInitializer - Module Startup Logic The `ApplicationModuleInitializer` interface allows modules to define initialization logic that runs on application startup in dependency order. ```java package com.example.inventory; import org.springframework.modulith.ApplicationModuleInitializer; import org.springframework.stereotype.Component; @Component public class InventoryInitializer implements ApplicationModuleInitializer { private final InventoryRepository repository; private final CacheManager cacheManager; @Override public void initialize() { // Runs after dependent modules are initialized log.info("Initializing inventory module"); // Warm up caches repository.findAllActive().forEach(item -> cacheManager.getCache("inventory").put(item.getSku(), item)); log.info("Inventory cache warmed with {} items", cacheManager.getCache("inventory").estimatedSize()); } } ``` ## Maven/Gradle Configuration Complete dependency configuration for using Spring Modulith in your project with event publication support. ```xml org.springframework.modulith spring-modulith-bom 2.0.5 pom import org.springframework.modulith spring-modulith-starter-core org.springframework.modulith spring-modulith-starter-test test org.springframework.modulith spring-modulith-starter-jdbc org.springframework.modulith spring-modulith-starter-jpa org.springframework.modulith spring-modulith-events-kafka org.springframework.modulith spring-modulith-actuator org.springframework.modulith spring-modulith-observability ``` ## Summary Spring Modulith excels in scenarios where a monolithic deployment benefits from clear internal boundaries. Its primary use cases include building modular monoliths that can later be split into microservices, enforcing architectural constraints through automated verification, implementing event-driven communication between modules with guaranteed delivery, and generating living documentation that stays synchronized with the actual code structure. The framework is particularly valuable for teams practicing domain-driven design who want to maintain bounded context integrity within a single deployable unit. Integration follows standard Spring Boot patterns - add dependencies, annotate your main class with `@Modulithic`, organize packages into modules, and use `@ApplicationModuleTest` for focused integration tests. Event-driven communication leverages Spring's `ApplicationEventPublisher` with `@ApplicationModuleListener` for consuming events, while the Event Publication Registry ensures no events are lost during failures. For external system integration, the `@Externalized` annotation or `EventExternalizationConfiguration` routes domain events to Kafka, RabbitMQ, or other messaging systems with transactional outbox semantics.