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.
| Element | Purpose | Boundary rule |
|---|---|---|
| Module API | Contract used by other modules | Only allowed entry point |
| Internal implementation | Domain logic and use cases | Invisible outside the module |
| Module data | Tables or schema owned by one module | No direct queries from other modules |
| Integration events | Asynchronous notification of changes | Published contract, versioned like an API |
| Host application | Starts modules, shares runtime and configuration | Contains no business logic |
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.
| Technique | Strength | Typical tooling |
|---|---|---|
| Separate build modules | Strong, compile-time | Maven or Gradle modules, .NET projects, Cargo crates |
| Language visibility | Strong within a module | Java modules, internal access modifiers, package-private classes |
| Architecture tests | Medium, build-time | ArchUnit, NetArchTest, dependency rule checkers |
| Schema per module | Medium, runtime | Separate database schemas and credentials |
| Code review conventions | Weak | Ownership files and review checklists |
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.
| Property | Traditional monolith | Modular monolith | Microservices |
|---|---|---|---|
| Deployment unit | One | One | Many, independent |
| Internal coupling | Often high | Controlled by boundaries | Low, enforced by network |
| Communication | Direct calls anywhere | In-process calls via module APIs | Network calls, messaging |
| Data consistency | Single transactions | Local transactions, events across modules | Eventual consistency, sagas |
| Operational cost | Low | Low | High, needs mature platform |
| Independent scaling | No | Limited | Yes, per service |
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.
| Symptom | Cause | Fix |
|---|---|---|
| Modules change together for every feature | Boundaries cut across the domain | Redraw modules around business capabilities |
| Queries join tables of several modules | Shared database without ownership | Introduce schemas per module and read models |
| A shared utility module keeps growing | Business logic placed in common code | Move logic back into the owning module |
| Circular dependencies between modules | Synchronous calls in both directions | Replace one direction with integration events |
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.