Spring Modulith
Keep a monolith healthy: express and verify module boundaries in code, so a modular monolith doesn't rot into a big ball of mud.
On this page
In the microservices module you learned the honest default: start with a monolith. But monoliths have a reputation for rotting - over years, every package reaches into every other, and you get a 'big ball of mud' that's impossible to change safely. Spring Modulith is Spring's answer: tools to keep a monolith modular on purpose, with boundaries you can actually verify.
The problem: boundaries that only exist in your head
You intend catalog, lending, and notification to be separate modules. But nothing stops a
developer from having catalog import and call a class deep inside lending's internals. Each such
shortcut erodes the boundary until the 'modules' are a fiction and the monolith is tangled. The Java
compiler won't complain - the boundaries live only in your head and the wiki.
The idea: modules as a first-class, verified concept
Spring Modulith treats each top-level package under your application as an application module, and gives you a test that fails if modules reach into each other's internals:
class ModularityTests {
@Test
void verifiesModuleBoundaries() {
ApplicationModules.of(BookVaultApplication.class).verify();
// fails the build if, say, catalog accesses lending's internal types
}
}The convention: a module's root package is its public API; anything in a sub-package is
internal and off-limits to other modules. Now the boundary is enforced by a test, not a promise.
Modulith can even generate documentation (C4-style diagrams) of your modules and their allowed
dependencies from the code itself.
Modules talk through events, not tangled calls
How should modules interact then? Preferably the same way services do in the microservices module:
by publishing application events rather than calling each other directly. BookVault already does
this - lending publishes BookBorrowedEvent, and a notifier reacts - so the modules are decoupled.
Modulith adds an event publication registry that persists events and can retry delivery to a
listener that failed, giving in-process events reliability closer to a broker's:
// lending doesn't call notification; it publishes. Modulith records and can redeliver the event.
events.publishEvent(new BookBorrowedEvent(isbn, memberId));An open-plan office (a monolith) is efficient - everyone's close. But with no walls at all, conversations bleed together and nobody can find quiet. Adding low, agreed partitions (module boundaries) keeps teams distinct and interactions deliberate - you talk to the next team through the doorway (a published event), not by leaning over their desks and grabbing their papers. Spring Modulith builds those partitions and checks nobody has knocked one down.
A well-modularized monolith de-risks a future split
Because Modulith modules already interact only through public APIs and events, each is a natural seam. If one module later needs to become its own service, you extract along a boundary that's already clean - exactly the 'draw the boundaries first' advice from the microservices module, now enforced by tooling.
A developer, in a hurry, imports com.bookvault.lending.internal.LoanEntity directly into the
catalog module to reuse a field. It compiles and the app runs. Explain how Spring Modulith turns
this silent erosion into a failing build, and why keeping the boundary matters for the monolith's
long-term health.
What does Spring Modulith help you do?
Key takeaways
- Monoliths rot when intended module boundaries aren't enforced - packages reach into each other until it's a big ball of mud.
- Spring Modulith makes each top-level package an application module, with its root as the public API and sub-packages internal.
- A verify() test fails the build if modules access each other's internals, so boundaries are enforced, not just documented.
- Modules should interact via published events; Modulith's event publication registry persists and can retry them for reliability.
- It can generate documentation/diagrams of modules from the code, and a clean-boundaried module is a natural seam for a future service split.