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

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:

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:

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

References