Modular monolith

On this page 7

A modular monolith is an application that is built, deployed and run as a single unit but is internally divided into independent modules with explicit boundaries, each owning its own logic and data and exposing only a deliberate public interface. In software architecture, it combines the operational simplicity of a monolith with much of the structural discipline of microservices.

The idea answers a common failure: classic monolithic architecture tends to decay into a tightly coupled codebase, while a premature move to microservices adds network calls, distributed data and heavy operations. A modular monolith keeps one process and one deployment pipeline, yet enforces the boundaries that would make a later split into services possible.

How a modular monolith is structured

Each module represents a business capability, such as ordering, billing or inventory, rather than a technical layer. Modules are usually aligned with bounded contexts from domain-driven design, so a term like customer can mean different things inside different modules without forcing a shared, bloated model.

A module consists of a public API, an internal implementation and a private data store. Other modules may call only the public API, typically through in-process interfaces or an internal event bus. Everything else, including domain classes, repositories and tables, is hidden from other modules, which is what distinguishes the style from a monolith that is merely organized into folders.

The following table lists the building blocks and the rule that each one enforces.

ElementPurposeBoundary rule
Module APIContract used by other modulesOnly allowed entry point
Internal implementationDomain logic and use casesInvisible outside the module
Module dataTables or schema owned by one moduleNo direct queries from other modules
Integration eventsAsynchronous notification of changesPublished contract, versioned like an API
Host applicationStarts modules, shares runtime and configurationContains no business logic
Building blocks of a modular monolith

Enforcing module boundaries

Boundaries that exist only in diagrams erode within months, so enforcement must be automated and part of the build. The strongest mechanisms come from the language and the build tool; weaker ones rely on review discipline. Teams usually combine a compile-time mechanism with architecture tests that fail the build when a forbidden dependency appears.

TechniqueStrengthTypical tooling
Separate build modulesStrong, compile-timeMaven or Gradle modules, .NET projects, Cargo crates
Language visibilityStrong within a moduleJava modules, internal access modifiers, package-private classes
Architecture testsMedium, build-timeArchUnit, NetArchTest, dependency rule checkers
Schema per moduleMedium, runtimeSeparate database schemas and credentials
Code review conventionsWeakOwnership files and review checklists
Boundary enforcement techniques

Data separation deserves particular attention. A shared database with cross-module joins is the fastest way to recreate a tightly coupled system. Giving each module its own schema, and allowing access only through the module API or published events, keeps data ownership explicit and makes the eventual extraction of a module far less painful.

Modular monolith compared with neighboring architectures

The modular monolith occupies the middle ground between a traditional monolith and a distributed system of microservices. The comparison below focuses on the properties that usually decide the choice: deployment, communication, data consistency and operational burden.

PropertyTraditional monolithModular monolithMicroservices
Deployment unitOneOneMany, independent
Internal couplingOften highControlled by boundariesLow, enforced by network
CommunicationDirect calls anywhereIn-process calls via module APIsNetwork calls, messaging
Data consistencySingle transactionsLocal transactions, events across modulesEventual consistency, sagas
Operational costLowLowHigh, needs mature platform
Independent scalingNoLimitedYes, per service
Comparison with related architectural styles

Unlike service-oriented architecture, which integrates separately deployed systems, and plugin architecture, which lets third parties extend a core, the modular monolith is about internal decomposition of one product owned by one organization.

When a modular monolith is the right choice

The style fits best when a system has real domain complexity but limited scaling divergence, meaning that most parts grow at similar rates. It suits teams of roughly a few to several dozen developers, products whose domain boundaries are still being discovered, and organizations that lack the platform engineering needed to operate dozens of services reliably.

  • Start here for new products when boundaries are uncertain, because moving code between modules is cheap while moving it between services is expensive.
  • Extract a module into a service only for a concrete reason, such as a very different scaling profile, a separate release cadence or strict isolation requirements.
  • Keep integration events in-process at first, and switch them to a message broker only when a module actually leaves the process.

A well-built modular monolith therefore works as a reversible architectural decision. Each module can become a microservice later, and a failed service split can be folded back into a module without rewriting the domain logic.

Limitations and common mistakes

The style keeps some monolithic constraints. All modules share one runtime, one release and one failure domain, so a memory leak in one module can stop the whole application, and a slow test suite in one area delays every release. Scaling remains mostly vertical or whole-application horizontal scaling.

SymptomCauseFix
Modules change together for every featureBoundaries cut across the domainRedraw modules around business capabilities
Queries join tables of several modulesShared database without ownershipIntroduce schemas per module and read models
A shared utility module keeps growingBusiness logic placed in common codeMove logic back into the owning module
Circular dependencies between modulesSynchronous calls in both directionsReplace one direction with integration events
Typical symptoms and fixes

In everyday language

Think of a large building with separate apartments. Everyone shares one roof, one address and one foundation, but each apartment has its own locked door, its own rooms and its own belongings. Neighbors talk through the front door, not by walking through each other’s walls.

A modular monolith is software built the same way. It is one program that is installed and started as a whole, yet inside it each part keeps to itself and cooperates only through agreed entrances. That order keeps the program easy to change and makes it possible to move one part into its own building later.

Seen in the real world

An online retailer with about twenty developers runs its shop, payments, warehouse and customer service functions in one aging application. Every change risks breaking an unrelated area, and a proposal to rewrite everything as thirty microservices is judged too costly for the small operations team.

Instead, the team reorganizes the codebase into six modules with separate database schemas, adds architecture tests to the build and replaces direct table access with module APIs and in-process events. Deployment stays a single container. Two years later, only the product search module, which needs far more capacity during sales, is extracted into its own service. The extraction takes weeks rather than months because its boundary had already been enforced.