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.

ArchUnitExample.javajava
@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.