Java modular monoliths are a single deployable application built from separate, well-defined modules that each handle a specific business capability. You use one when you want the operational simplicity of a monolith with the code-level discipline of modular design, typically for teams of 10–100 developers working on a complex domain. This article explains what makes a modular monolith different from a regular monolith or microservices, how to structure one in Java, and the concrete trade-offs to consider before adopting this architecture.
Background: From Monolith to Microservices and Back
For years, the default enterprise Java architecture was a single WAR or JAR file containing everything: controllers, services, repositories, and domain logic. That worked well until codebases grew into “big balls of mud� where every class depended on every other class. Microservices emerged as a reaction, splitting the application into independently deployable services. But microservices bring their own costs: distributed transactions, network latency, eventual consistency, and complex orchestration. Many teams found they had traded a code problem for an infrastructure problem.
The modular monolith sits in the middle. It keeps everything in one deployment unit, but enforces strong boundaries between modules. This idea is not new—OSGi and Java EE application modules tried it years ago—but modern tools like Java Platform Module System (JPMS, introduced in Java 9), ArchUnit, and build tools like Gradle and Maven make it practical without the operational overhead of separate services.
How a Modular Monolith Works in Java
In a modular monolith, you divide the application into modules that each own a slice of business functionality. A typical e-commerce app might have modules for catalog, orders, inventory, customer, and shipping. Each module has its own:
- Domain model – entities, value objects, and business rules.
- Application services – use cases and orchestration within the module.
- Persistence – its own JPA entities, repositories, and database schema (or at least its own tables).
- API surface – public interfaces that other modules can call, often defined as plain Java interfaces or events.
Modules communicate through well-defined interfaces, not by reaching into each other’s internals. For example, the orders module might expose an OrderService interface, and the catalog module might publish a ProductPriceChangedEvent that orders subscribes to. In-process calls are still fast and simple, but the boundaries force you to think about dependencies.
Java 9’s JPMS provides module-info.java files that explicitly declare which packages are exported and which are internal. However, many teams find JPMS too strict for large applications and instead rely on conventions plus ArchUnit tests to enforce boundaries. The key is that the architecture is enforced, not just aspirational.
How to Choose: Is a Modular Monolith Right for You?
Consider a modular monolith when:
- Your team is small to medium (roughly 10–100 developers) and you want to avoid the overhead of running many services.
- Your domain has clear subdomains or business capabilities that map naturally to modules.
- You need strong consistency within a business transaction (e.g., order and inventory updates must be atomic).
- You are starting a new project and want to keep the option of extracting modules into microservices later.
- Your organization values operational simplicity: one deployment, one log stream, one monitoring dashboard.
Choose microservices instead when you have independently scaling components, teams that need to deploy on their own schedules, or a need to use different technology stacks per service. Choose a plain monolith when the application is small and simple—adding module boundaries would be over-engineering.
Common Problems and How to Avoid Them
The biggest risk is that modules become “porous� and the boundaries erode over time. This happens when developers take shortcuts, like importing a class from another module’s internal package. To prevent this:
- Use ArchUnit to write tests that fail if a module depends on another module’s internals.
- Keep module APIs small and stable. Prefer events over direct calls for cross-module communication when you want loose coupling.
- Do not share JPA entities across modules. Each module owns its persistence; if another module needs data, expose a query method.
- Watch for “sneaky� dependencies through static utility classes or thread-local context. Make these explicit or eliminate them.
Another common issue is the “distributed monolith� anti-pattern: you modularize the code but still deploy as one unit, yet you introduce asynchronous messaging and eventual consistency between modules. That adds complexity without the benefit of independent scaling. Keep inter-module communication synchronous unless you have a clear reason for async.
Comparison: Modular Monolith vs. Microservices vs. Traditional Monolith
Here’s a quick comparison of the three approaches:
- Deployment: Traditional monolith and modular monolith deploy as one unit; microservices deploy as many units.
- Scalability: Microservices scale each service independently; modular monolith scales the whole application (though you can still scale horizontally).
- Team autonomy: Microservices allow each team to own a service end-to-end; modular monolith requires coordination on the shared codebase and release cadence.
- Transactionality: Monoliths (both types) can use ACID transactions across modules; microservices typically need sagas or eventual consistency.
- Complexity: Traditional monolith is simplest initially; microservices are most complex; modular monolith is a middle ground.
In practice, many teams start with a modular monolith and later extract a few modules into microservices when they need independent scaling or team autonomy. The module boundaries you defined become the seams for extraction.
Costs and Trade-offs
Adopting a modular monolith requires upfront discipline. You must invest in:
- Design time to identify module boundaries and define APIs.
- Tooling to enforce boundaries (ArchUnit, dependency analysis tools).
- Refactoring if you are converting an existing monolith; this can be significant if the codebase is tightly coupled.
- Team training so everyone understands the rules and follows them.
The payoff is lower operational overhead compared to microservices, and better maintainability compared to a traditional monolith. You also avoid the network latency and distributed debugging that plague microservices. However, you still have a single point of failure and you cannot scale individual modules independently. If one module has a memory leak, it affects the whole application.
Safety and Operational Considerations
From an operational perspective, a modular monolith is as safe as any single application. You should follow standard practices: health checks, structured logging, centralized monitoring, and graceful shutdown. Because everything runs in one process, you can use thread dumps and profilers to diagnose issues across modules. However, be careful with classpath conflicts: if two modules depend on different versions of the same library, you may need to shade or isolate them, which adds complexity.
There is no special “safety� issue unique to modular monoliths, but the same rules apply as for any Java application: keep dependencies up to date, run tests in CI, and have a rollback plan. The modular structure actually helps safety because you can test modules in isolation, and the explicit boundaries make it easier to reason about the impact of changes.
Alternatives to a Modular Monolith
If a modular monolith does not fit, consider these alternatives:
- Traditional monolith – for very small applications or prototypes where speed of delivery trumps long-term maintainability.
- Microservices – when you need independent scaling, team autonomy, or polyglot persistence.
- Clean Architecture / Hexagonal Architecture – a design pattern that can be applied within a monolith to separate business logic from infrastructure, without necessarily creating deployable modules.
- Modular microservices – a hybrid where each microservice is itself built as a modular monolith.
There is also the “modulith� pattern popularized by Spring Modulith, which provides tooling to implement and verify module boundaries in Spring Boot applications. That is a concrete way to get started without building your own enforcement infrastructure.
Frequently Asked Questions
Is a modular monolith just a monolith with good packages?
No. Good packages are a start, but a modular monolith requires explicit boundaries, often enforced by tooling, and each module owns its data and domain logic. It is a structural pattern, not just a naming convention.
Can I extract a module into a microservice later?
Yes, that is one of the main benefits. If modules communicate through well-defined interfaces and do not share database tables, you can extract a module into a separate service by replacing in-process calls with HTTP or messaging. The extraction is still work, but the boundaries are already in place.
Does a modular monolith work with Spring Boot?
Yes. Spring Boot works well; you can use Spring Modulith to define modules and verify dependencies. You can also use plain Java with ArchUnit. The key is to enforce boundaries, regardless of framework.
How do modules share data without sharing JPA entities?
Each module owns its tables and entities. If another module needs data, it calls a public method on the owning module’s service. For read-only queries, you can expose a query API or use a read model that is updated via events. Avoid direct database access across modules.
What is the difference between a modular monolith and a microservice with a shared database?
A microservice with a shared database is usually an anti-pattern because services are tightly coupled through the schema. A modular monolith, by contrast, enforces that each module owns its schema, and other modules access data only through the module’s API. The deployment unit is the same, but the coupling is controlled.
Related
Formula used
in t | Finish time | min | 50 | 0 in d | Distance | km | 10 | 0 | 100000 out pace | Average pace | min/km | t / d | 2 out speed | Average speed | km/h | d / (t / 60) | 2
Each line is one input (in, sel, list) or one result (out: label, unit, formula, decimals). How the calculators work
Method: the standard formula for this calculation, rounded as each result states. See how the calculators work or report an error.
More cards in this drawer
- Valheim Server Not Showing in Community List Fixes
- DisplayPort vs HDMI: Which Cable Should You Use?
- Soundbar vs 2.1 Speaker System: Which Is Better?
- Survive Lava for Brainrot Tier List170 points
- MySQL Dump Import: Fix Common Errors
- Mouse Moving on Its Own? Causes & Fixes