Modular Monoliths
Microservices promise independent teams and deployments, but they come with network failures, distributed transactions, eventual consistency, versioned APIs, and a large operational platform. Many teams adopt them to escape a tangled monolith — and end up with a distributed monolith: services that must be deployed together because their boundaries were never right. The problem was never the single deployable unit; it was the missing boundaries inside it.
A modular monolith keeps one deployable application while dividing it into modules with explicit public APIs, hidden internals, and their own data. You get most of the design benefits of services — clear ownership, independent reasoning, enforceable boundaries — while keeping in-process calls, local transactions, one deployment, and simple operations. And because the boundaries are real, extracting a module into a service later becomes a contained project rather than a rewrite.
TL;DR
- One deployable unit, divided into modules aligned with business capabilities or bounded contexts.
- Each module exposes a public API (interfaces, commands, events) and keeps everything else internal.
- Modules own their data: separate schemas or tables, no cross-module joins or writes.
- Communicate through in-process calls to public APIs or in-process events, kept free of cycles.
- Enforce boundaries automatically with tools such as Spring Modulith, ArchUnit, Packwerk, Nx, or language visibility rules.
- Extract a module into a service only for a concrete reason — independent scaling, release cadence, or isolation.
Quick Example
A package layout for a Spring Boot modular monolith, where each top-level package is a module:
Publishing an event from one module and handling it in another, without a direct dependency:
A test that fails when modules form cycles or reach into each other's internals:
Core Concepts
Modules and Boundaries
A module is a cohesive unit around a business capability — orders, inventory, billing, identity — with:
- A public API: the services, commands, queries, and events other modules may use.
- Internals: entities, repositories, rules, and adapters no other module may touch.
- Owned data: tables or a schema only it reads and writes.
- An owner: a team or group responsible for its changes.
Draw boundaries from the domain, not from technical layers. "Controllers", "services", and "repositories" are layers inside a module. Domain-driven design's bounded contexts are a good guide. See Bounded Contexts and Context Mapping and Domain-Driven Design.
Communication Between Modules
Keep the dependency graph between modules acyclic. Event-based communication decouples modules, but handlers that run after commit need retries or an event publication log so failures are not lost — Spring Modulith keeps one, and the Outbox Pattern achieves the same in other stacks.
Data Ownership
Modules sharing tables are coupled no matter how clean their code is. Give each module its own schema (or table prefix) with a database role allowed to access only its tables, and forbid cross-module joins and writes. When a module needs another's data, it calls the public API or keeps a local read model updated by events. A single database still allows local transactions within a module, which is a major simplification compared with services.
Enforcing Boundaries
Boundaries that are not enforced erode quickly; run checks in CI. See Architecture Fitness Functions.
Testing
Test each module through its public API with its own data, faking or stubbing other modules' APIs. Spring Modulith can bootstrap a single module for integration tests; in other stacks, construct the module with in-memory adapters. A small set of end-to-end tests covers flows across modules.
When to Extract a Service
A module is a good extraction candidate when it needs:
- Independent scaling, such as a CPU-heavy reporting or media-processing module.
- A different release cadence or a separate team that is blocked by shared deploys.
- Stronger isolation for security, compliance, or fault containment.
- A different technology, such as a specialized database or language.
Because the module already has a public API and owned data, extraction means replacing in-process calls with network calls (adding timeouts, retries, and idempotency), moving its schema to a separate database, and turning in-process events into messages. See Microservices and Monolith vs Microservices.
Best Practices
Start With Few, Coarse Modules
Begin with a handful of modules aligned with major business capabilities and split them when they grow, rather than designing dozens up front.
Make the Public API Small
Expose only what other modules need; everything else stays internal and free to change.
Give Each Module Its Own Data
Separate schemas and database permissions per module turn data ownership from a convention into a rule.
Verify Boundaries in CI
Run module verification and dependency checks on every build.
Common Mistakes
Layers Instead of Modules
Shared Tables Across Modules
Extracting Services Without a Reason
FAQ
What is a modular monolith?
An application deployed as a single unit but structured into modules with explicit public APIs, hidden internals, and owned data, so boundaries are enforced without distributing the system.
How is a modular monolith different from microservices?
Modules communicate in-process and deploy together, sharing one runtime and often one database instance; microservices communicate over the network and deploy independently. The modular monolith avoids distributed-systems complexity while keeping clear boundaries.
Can modules share a database?
They can share a database server, but each module should own its tables or schema exclusively, with no cross-module joins or writes, so data ownership stays clear.
How do I enforce module boundaries?
Use language visibility, build modules or packages, and architecture tests — such as Spring Modulith, ArchUnit, Packwerk, Nx boundary rules, or import-linter — run in CI.
When should a module become a service?
When it has a concrete need that a shared deployment cannot meet: independent scaling, release cadence, technology, isolation, or team autonomy.
Related Topics
- Architecture — Designing software systems
- Monolith vs Microservices — Choosing between deployment models
- Domain-Driven Design — Modeling the domain to find boundaries
- Bounded Contexts and Context Mapping — Defining module boundaries
- Microservices — The distributed alternative
- Hexagonal Architecture — Structuring each module internally