Modular Monolith
A modular monolith is a single deployable unit whose internal boundaries are enforced: each module owns its data, exposes an explicit interface, and cannot reach into another module’s internals. It is one answer to the observation that most of the benefit attributed to splitting a system comes from having boundaries, and most of the cost comes from those boundaries being network calls.
The term is industry usage rather than a standardized definition, so the description above is the working definition for this article. A single deployable is compatible with cloud native principles: CNCFโs definition lists microservices as one possible technology, rather than making service decomposition mandatory.
What makes it modular is enforcement
Folders alone do not enforce a boundary. Visibility rules, dependency checks, data permissions, and review each constrain different ways of crossing it. A modular design states the permitted interface and checks that modules use it.
- Language-level visibility. Use modules, packages, or assemblies to expose the intended interface and restrict direct access to implementation details within the languageโs supported rules.
- Build-time dependency rules. A check that fails when module A imports module B’s internals, or when a dependency creates a cycle. Catches what visibility alone cannot express.
- Data ownership per module. Route writes through the owning module and avoid consumers reaching directly into its tables. Schema and database permissions help where distinct access identities can be enforced; a shared broad credential does not distinguish which module issued a query.
- Explicit interfaces between modules, including in-process events where appropriate. A published interface makes dependencies visible and reduces the implementation details a later extraction must untangle. It still needs assessment as a remote contract.
Shared tables can make extraction require a data migration as well as code changes. Before moving a module, identify the authoritative records, their readers and writers, and the contract that will replace direct access. Separating these responsibilities early reduces the work required later.
A bounded context is a useful starting point: it marks where a domain model and its vocabulary have a consistent meaning. Responsibility for maintaining the model is an additional organizational choice. As Microsoftโs microservice-boundary guidance illustrates, one context can contain several service candidates; consistency and operating requirements help determine which should share a deployment.
What it buys, and what it defers
| Modular monolith | Separate services | |
|---|---|---|
| Boundary enforcement | Visibility and dependency rules, with controlled data access | Explicit remote interfaces; permissions and contract rules remain necessary |
| Calls across a boundary | Ordinary synchronous calls return or raise directly; process crashes and async timeouts remain possible | Network latency, independent request/response failures, and retry decisions |
| Deployment | One deployable is released together | Separate releases when supported API, event, and data contracts remain compatible |
| Local development and testing | One application process; data and external dependencies still affect test cost | More runtime dependencies to arrange; speed and stability depend on test scope and environment |
| Scaling | The whole unit scales together | Per service, which matters when one part is much hotter |
| Failure isolation | A process crash affects all modules; separate pools can contain some resource failures | Contains process failures; shared infrastructure, pools, and dependencies still need assessment |
Separate services offer independent deployment, scaling, and process-level fault containment. They also add operating and communication costs. Isolation at the level of a userโs task depends on the resources and dependencies still shared: removing synchronous calls does not isolate a common broker or database. Microsoftโs Bulkhead guidance describes separating pools and queues to contain specific failures. A modular monolith is a useful default when those additional boundaries do not yet justify their cost.
Twelve-Factorโs state guidance applies to either deployment style: keep persistent application state in a backing service, while allowing rebuildable caches or temporary files within one execution. Memory-only sessions can be spread across several instances using session affinity, but replacing an instance does not preserve its sessions. AWS documents that routing option; Twelve-Factor recommends external session storage instead. Replaceability also requires handling in-flight work and restart, as its disposability guidance explains.
When to extract a module
Extraction is a project with migration and operating costs. The following needs can justify evaluating it; whether it is worthwhile depends on the current constraints and the teamโs ability to run the result.
- A scaling profile unlike the rest of the system, where scaling everything to serve one hot component is genuinely wasteful.
- A different reliability requirement, where one part must survive the failure of another โ the case where isolation is the point rather than a side effect.
- A release cadence that cannot be shared, for regulatory, vendor, or contractual reasons.
- A different runtime, where the work needs a language or platform the main deployable cannot host.
Modularity reduces the amount of behavior and data that must be untangled. It does not make extraction a mechanical transport change. Fowlerโs discussion of distributed objects explains why a good local interface may need a different remote shape: repeated small calls can become prohibitively slow, and object references must become a serializable contract. Authentication, supported contract versions, and deployment operations also need design.
Prepare by keeping the interface explicit, identifying transactions that cross modules, and measuring the calls the proposed service would receive. Check which data must move, how consumers migrate, and how failures affect their work. A module with many dependents may still be extractable, but it has a wider compatibility and rollout problem. A narrow interface alone does not establish that those costs are acceptable.
How it fails
Boundary erosion can occur in both code and data access. These are useful places to inspect before planning an extraction.
Modules that are only a naming convention. Without visibility rules or dependency checks, violations are harder to detect consistently and review carries more of the burden. The absence of a build check is a detection gap, not proof that all boundaries have already failed. Inspect actual imports and calls, then automate the rules that matter.
Shared tables. Modules may look separate in code while depending on one storage representation. Some schema changes are compatible, but others require every consumer to migrate. Make those consumers and contracts explicit before moving the dependency across a network.
A third, milder failure is treating the structure as permanent in the other direction: refusing to extract anything on principle, including the component with a genuinely different reliability requirement. The point of the approach is to defer a cost, not to forbid ever paying it. How this choice fits with data ownership, Conway’s law, and what a split charges you is worked through in Splitting the System Was the Easy Part.
References
- CNCF โ Cloud Native Definition v1.1
- The Twelve-Factor App โ Processes
- The Twelve-Factor App โ Disposability
- AWS โ Application Load Balancer target group attributes
- Microsoft โ Bulkhead pattern
- Python โ concurrent.futures
- Microsoft โ Identify microservice boundaries
- Martin Fowler โ Microservices and the First Law of Distributed Objects
- James Lewis & Martin Fowler โ Microservices
Discover more from Insightful Data Lab
Subscribe to get the latest posts sent to your email.
