Back to Blog
spring-modulithspring-bootarchitecturemodulithmicroservicesjava

Spring Modulith: Build a Modular Monolith That Can Become Microservices (2026)

Microservices add distributed systems complexity before you've earned it. Spring Modulith gives you module boundaries, event-driven communication, and clear architecture — without the network overhead.

J

JOptimize Team

May 29, 2026· 10 min read

The microservices debate has matured. The industry has learned that splitting a codebase into dozens of services before you understand your domain creates distributed systems problems — network latency, distributed transactions, complex deployments — without providing the benefits you hoped for. The modulith is the answer to this: strong module boundaries, clear architecture, and event-driven communication, all within a single deployable unit.

Spring Modulith is Spring's framework for building modular monoliths. It enforces module boundaries at development time, provides event-driven communication between modules, and generates architectural documentation automatically.


The Problem with Both Extremes

A big ball of mud monolith has no boundaries. Any class can call any other class. The order service reaches directly into the inventory database. The billing module reads user tables it shouldn't know about. Over time, every change breaks something unexpected, and the codebase becomes unmaintainable.

Premature microservices solve the coupling problem by making modules into separate processes — but introduce distributed systems complexity: network failures, eventual consistency, distributed tracing, service discovery, and deployment pipelines for 15 services instead of one. Most teams are not ready for this complexity on day one.

The modulith sits between these extremes. Each module is a self-contained unit with its own public API. Modules communicate through events, not direct method calls. The architectural rules are enforced by the framework. But everything runs in a single JVM, so you get transactions, simple deployment, and fast integration tests.


Structure: Modules as Java Packages

In Spring Modulith, a module is simply a top-level package under your application package. The framework discovers modules automatically:

com.example.shop/
├── ShopApplication.java
├── order/                    ← Order module
│   ├── OrderController.java  (public API)
│   ├── OrderService.java     (public API)
│   ├── Order.java            (public API)
│   └── internal/             ← Internal — not accessible from other modules
│       ├── OrderRepository.java
│       ├── OrderValidator.java
│       └── OrderMapper.java
├── inventory/                ← Inventory module
│   ├── InventoryService.java (public API)
│   └── internal/
│       ├── InventoryRepository.java
│       └── StockLevel.java
└── notification/             ← Notification module
    ├── NotificationService.java
    └── internal/
        └── EmailSender.java

Anything in an internal subpackage is module-private. Spring Modulith enforces this at test time — if the order module tries to access InventoryRepository directly, the architecture test fails.

@SpringBootApplication @EnableSpringDataWebSupport public class ShopApplication { public static void main(String[] args) { SpringApplication.run(ShopApplication.class, args); } }

No special annotations needed to define modules. The package structure IS the architecture.


Enforcing Module Boundaries

Spring Modulith provides a test that verifies your architecture rules are not violated:

@SpringBootTest public class ModularityTests { ApplicationModules modules = ApplicationModules.of(ShopApplication.class); @Test void verifyModularStructure() { modules.verify(); // Fails if: // - Module A accesses Module B's internal classes // - There are circular dependencies between modules // - A module accesses another module's persistence layer directly } @Test void printModuleStructure() { modules.forEach(System.out::println); // Prints the module graph — useful for onboarding } }

This test runs in CI. Violations fail the build. You don't need code reviews to enforce architecture — the framework does it.


Inter-Module Communication: Application Events

Modules should not call each other directly. Direct calls create tight coupling — the order module needs to know about the inventory module's API, the notification module, and anything else that needs to happen when an order is placed.

Spring Modulith uses Spring's ApplicationEventPublisher for loose coupling between modules:

// Order module: publishes an event, doesn't know who listens @Service @RequiredArgsConstructor public class OrderService { private final OrderRepository orderRepo; private final ApplicationEventPublisher events; @Transactional public Order placeOrder(PlaceOrderRequest req) { Order order = orderRepo.save(Order.from(req)); // Publish event — order module doesn't know about inventory or notifications events.publishEvent(new OrderPlacedEvent(order.getId(), order.getItems())); return order; } } // Define the event in the order module (it's part of its public API) public record OrderPlacedEvent(Long orderId, List<OrderItem> items) {}
// Inventory module: listens without the order module knowing @Component public class InventoryEventHandler { @ApplicationModuleListener // Spring Modulith annotation — async by default public void onOrderPlaced(OrderPlacedEvent event) { event.items().forEach(item -> inventoryService.reserveStock(item.productId(), item.quantity()) ); } } // Notification module: also listens independently @Component public class NotificationEventHandler { @ApplicationModuleListener public void onOrderPlaced(OrderPlacedEvent event) { notificationService.sendOrderConfirmation(event.orderId()); } }

@ApplicationModuleListener is Spring Modulith's annotation that makes event processing transactional and asynchronous by default. If the inventory reservation fails, Spring Modulith can retry the event. The order transaction is not affected.


Event Externalization — The Bridge to Microservices

Here's where Spring Modulith becomes strategically powerful. When you're ready to extract a module into a separate service, you externalize its events to a message broker:

// Mark event for externalization to Kafka @Externalized("shop.orders") public record OrderPlacedEvent(Long orderId, List<OrderItem> items) {}
spring: modulith: events: kafka: enabled: true

With this configuration, Spring Modulith automatically publishes OrderPlacedEvent to the shop.orders Kafka topic when it's raised. In-process listeners still work — you've added Kafka without changing anything else.

When you later extract the notification module into its own service, it subscribes to the same Kafka topic. The order module code doesn't change at all. This is the migration path from modulith to microservices that doesn't require a big-bang rewrite.


Generated Architecture Documentation

Spring Modulith can generate C4 component diagrams and PlantUML from your module structure:

@Test void generateDocumentation() throws Exception { new Documenter(ApplicationModules.of(ShopApplication.class)) .writeModulesAsPlantUml() // generates modules.puml .writeIndividualModulesAsPlantUml() // one diagram per module .writeAggregatingDocument(); // HTML report }

The documentation is generated from code, so it's always accurate. No more architecture diagrams that drift from reality.


Integration Testing Modules in Isolation

One of the biggest practical benefits of Spring Modulith is isolated module testing. You can boot only the modules you need:

@ApplicationModuleTest // Boots only the order module and its dependencies class OrderModuleTests { @Test void placingOrderPublishesEvent( @Autowired OrderService orderService, PublishedEvents events) { orderService.placeOrder(new PlaceOrderRequest(/* ... */)); // Verify event was published without starting inventory or notification modules events.ofType(OrderPlacedEvent.class) .matching(OrderPlacedEvent::orderId, 1L) .hasSize(1); } }

This makes integration tests significantly faster. You're not starting the entire application context for every module test.


When to Choose Modulith Over Microservices

Choose Spring Modulith when:

  • You're starting a new project and the domain boundaries aren't fully understood yet
  • Your team is small (under 8-10 engineers) and doesn't have dedicated DevOps for service orchestration
  • You want strong architecture without distributed systems complexity
  • You anticipate extracting services later as the team and domain scale

Choose microservices when:

  • Different modules need to scale independently by orders of magnitude
  • Different teams need to deploy independently without coordination
  • Modules have genuinely different technology requirements (one needs Python, one needs Java)
  • The organization is large enough that Conway's Law makes separate deployables necessary

The Spring Modulith path is clear: start with a well-structured modulith, and extract microservices when there's a concrete, measurable reason to do so.


Common Mistakes to Avoid

  • Accessing another module's repository directly — this defeats the purpose of modules; always go through the module's public service API
  • Putting everything in the root package — classes in the application root package are accessible to all modules by default, creating a "shared utils" antipattern
  • Synchronous events for everything@ApplicationModuleListener is async by default; using synchronous events for cross-module calls recreates the coupling you were trying to avoid
  • Not running modules.verify() in CI — the architecture test is only useful if it fails the build

Summary

Spring Modulith offers a pragmatic middle path between the big ball of mud and premature microservices. Package-based module boundaries, event-driven inter-module communication, automated architecture verification, and a clear migration path to microservices when needed. For most greenfield Java applications in 2026, starting with a well-structured modulith is the right architectural choice.


Keep Your Modulith Performant

JOptimize analyzes your Spring application — modulith or microservice — for N+1 queries, missing indexes, and inefficient data access patterns that often hide inside module boundaries.

Good architecture and good performance go hand in hand.

Want to go deeper?

Master Spring Boot, security, and Java performance with hands-on courses.

Detect issues in your project

JOptimize finds N+1 queries, EAGER collections, and 70+ other issues in your Java codebase — in under 30 seconds.