# 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.