Microservices introduce incredible complexity: distributed tracing, network latency, eventual consistency, and complex deployment pipelines. A Modular Monolith allows you to reap the organizational benefits of microservices while keeping deployment and infrastructure simple.
Module 1: The Fallacy of Microservices First
Many teams adopt microservices to solve organizational scaling issues rather than technical scaling issues. They end up with a 'Distributed Monolith' where services share a single database, defeating the purpose.
Modular Monolith Core Tenets
- Single Deployment Unit: The entire application is deployed as one artifact.
- Strict Boundaries: Modules cannot directly access each other's database tables or internal classes.
- In-Process Communication: Modules communicate via in-memory events or public interfaces rather than network calls.
Module 2: Enforcing Boundaries
A true modular monolith requires structural enforcement. You can achieve this using architectural fitness functions and explicit module APIs.
@Test
public void billingModuleShouldNotDependOnShipping() {
noClasses()
.that().resideInAPackage("..billing..")
.should().dependOnClassesThat().resideInAPackage("..shipping..")
.check(importedClasses);
}Module 3: Transitioning to Microservices
When a specific module finally requires independent scaling or deployment, extracting it from a well-structured modular monolith is trivial compared to untangling a spaghetti monolith.