In modern software development, maintaining a clean, modular architecture is essential for scalability, maintainability, and long-term agility. While Spring Boot has long been the go-to framework for building microservices and monoliths alike, it hasn’t always made it easy to enforce modular boundaries within a single application. That’s where Spring Modulith comes in—a new addition to the Spring ecosystem that brings first-class support for building truly modular monolithic applications. In this post, we’ll explore what Spring Modulith is, why it matters, and how it can help you design better-structured, more maintainable Spring-based systems without the overhead of full microservices.
What’s modulith
A modulith is a monolithic application designed with a strong internal modular structure. Unlike traditional monoliths, where all components are tightly coupled and often difficult to maintain, a modulith explicitly defines clear boundaries between its internal modules. Each module encapsulates its own logic, data access, and services, exposing only well-defined interfaces for communication. This modular approach allows teams to work on different parts of the application independently, reduces the risk of unintended side effects, and lays a solid foundation for future scaling—whether that means scaling the team or evolving toward microservices. Moduliths combine the simplicity of monolithic deployment with the architectural discipline typically associated with distributed systems.
In a modulith, boundaries between modules are defined both structurally and semantically. Structurally, each module resides in its own package or namespace, containing all the necessary components such as entities, services, and repositories. These modules avoid leaking implementation details by exposing only public APIs or application events. Semantically, the boundaries are enforced through architectural conventions and tools—like those provided by Spring Modulith—that validate dependencies and prevent cross-module coupling. This means one module cannot directly access internal classes of another, encouraging loose coupling and high cohesion. By treating each module as an independent unit with its own responsibilities and lifecycle, a modulith promotes better separation of concerns and a more maintainable codebase.
ApplicationModule
Here’s a practical example of how to define and work with modules using Spring Modulith, focusing on the ApplicationModule
abstraction and the enforcement of boundaries.
Let’s say we’re building a blog platform with three modules:
-
Authoring
– manages posts and drafts -
Publishing
– handles publication and scheduling -
Analytics
– tracks views and engagement
Each module lives in its own package:
com.example.blog.authoring
com.example.blog.publishing
com.example.blog.analytics
Spring Modulith allows you to mark a package as a module using the @ApplicationModule
annotation. For example, in com.example.blog.authoring
:
@ApplicationModule
package com.example.blog.authoring;
import org.springframework.modulith.ApplicationModule;
This tells Spring Modulith that everything in authoring
is part of a self-contained module with its own public API, internal classes, and dependencies. Imagine you try to access a service inside Authoring
that hasn’t been exposed via a public API. When you run such, Spring Modulith will throw a ModuleViolationException, showing a report of which class accessed what and how it broke modular rules.
Modulith and DDD
Spring Modulith aligns naturally with Domain-Driven Design (DDD) by encouraging a clear separation of bounded contexts and encapsulation of domain logic. In DDD, each bounded context represents a cohesive part of the domain with its own model and rules—this maps directly to a Modulith module. With @ApplicationModule
, you can define each bounded context as an independent module, isolating its aggregates, repositories, and services. Modulith enforces the boundaries between these contexts, ensuring they communicate only through well-defined interfaces or domain events. This structure supports strategic design, prevents accidental coupling, and promotes a clean, maintainable domain model. By combining DDD principles with Modulith’s tooling, developers can build monoliths that are modular in design but still consistent in deployment.
Instead of direct service calls, you can use domain events to interact across modules. For instance:
// In Publishing module
applicationEventPublisher.publishEvent(new PostPublishedEvent(postId));
// In Authoring module
@EventListener
void handle(PostPublishedEvent event) {
// mark post as published
}
This approach maintains clean boundaries while allowing loosely-coupled interaction.
Seamless Service Extraction with Spring Modulith
By using Spring Modulith’s tools—such as @ApplicationModule
, explicit API boundaries, and event-driven communication—you naturally structure your monolith in a way that mirrors service-oriented architecture. Each module behaves like a mini-application with its own encapsulated logic and defined interfaces. This makes it much easier to extract a module into a standalone service later if needed. Since the module already communicates with others through events or well-defined APIs, the code and dependencies are decoupled and self-contained. As a result, migrating a module to a separate microservice becomes a matter of moving it into a new project and re-routing its communication, rather than untangling tightly coupled logic. Spring Modulith essentially future-proofs your monolith by making it modular by design, not just by intention.
Let’s walk through an example of how using Spring Modulith enables an easy migration of a module from a monolith to an independent microservice.
- Move package into its own Spring Boot project. All logic is already self-contained and respects module boundaries.
2. Expose an endpoint for receiving events:
@RestController
@RequestMapping("/events")
public class EventController {
@PostMapping("/published")
public void handlePublished(@RequestBody PublishedEvent event) {
viewTracker.initializeMetrics(event.getId());
}
}
3. Update the original monolith to send HTTP requests instead of publishing events:
public class RemoteAdapter {
private final WebClient webClient;
public Remotedapter(WebClient.Builder builder) {
this.webClient = builder.baseUrl("http://service").build();
}
public void notifyPublished(PublishedEvent event) {
webClient.post()
.uri("/events/published")
.bodyValue(event)
.retrieve()
.toBodilessEntity()
.subscribe();
}
}
Because Spring Modulith enforced clean boundaries and event-based communication, the migration is straightforward:
- No shared internal logic
- No tight coupling
- Minimal refactoring required
This illustrates how Modulith lets you start with a monolith and grow into microservices when needed—without a complete architectural rewrite.
Learn more: Spring Modulith Official Project Page
Leave a Reply