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 mine. What is documented is that separate services are not a requirement of being cloud native: the CNCF definition names “loosely coupled systems” as the characteristic and lists microservices among technologies that architectures “typically consist of some combination of,” describing that list as “non-exhaustive.”
What makes it modular is enforcement
Every codebase has folders, and folders are not boundaries. The difference is whether crossing one incorrectly fails — at compile time, in a check, or in review — rather than being discouraged. Four mechanisms do the enforcing, roughly in order of strength.
- Language-level visibility. Separate modules, packages, or assemblies where only the intended interface is public. The strongest option because it is not optional.
- 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. Each module’s tables are written only by that module, with no cross-module joins — enforced by schema separation and database permissions where the engine allows. This is the boundary that matters most and the one most often skipped.
- Explicit interfaces between modules, including in-process events where the coupling should be one-directional. Calling a published interface rather than an internal class is what makes a later extraction mechanical.
The third item deserves emphasis because it is the one that determines whether extraction is ever possible. Two modules sharing tables are one module with two names, and separating them later is a data migration rather than a refactor.
Where the boundaries should fall is a separate question with a better vocabulary: a bounded context is a region within which a term has one meaning and one owner, and it is a more reliable guide than technical layering.
What it buys, and what it defers
| Modular monolith | Separate services | |
|---|---|---|
| Boundary enforcement | In code — strong, and possible to cheat with sufficient determination | By the network — stronger, and impossible to cheat accidentally |
| Calls across a boundary | In-process: fast, shared fate, and the caller sees the exception | Network: latency, timeouts, retries, and a reply that can be lost after the work is done |
| Deployment | One unit — a change anywhere releases everything | Independent, provided data and meaning are also separated |
| Local development and testing | Runs on one machine; end-to-end tests stay fast | Needs an environment; end-to-end tests become slow and flaky |
| Scaling | The whole unit scales together | Per service, which matters when one part is much hotter |
| Failure isolation | Weak — a leak or a saturated thread pool affects everything | Real, where services do not depend on each other synchronously |
The two rows separate services genuinely win are independent deployment and failure isolation. Everything else on the list favours the single deployable, which is why modular monolith first is a defensible default rather than a conservative one.
One property is shared and worth keeping regardless: the Twelve-Factor guidance that “twelve-factor processes are stateless and share-nothing” and that “any data that needs to persist must be stored in a stateful backing service, typically a database,” applies to a monolith exactly as it does to a service. A single deployable that keeps session state in memory cannot be scaled horizontally or replaced freely, which removes most of the operational benefit of either architecture.
When to extract a module
Extraction is a project, so it wants a reason that will still look like a reason in a year. Four qualify.
- 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.
Absent one of those, the anticipated future need is not a reason to pay now. And when the time comes, extraction from a properly modularized deployable is mechanical: the interface already exists, the data is already separated, and the change is mostly transport plus the failure handling a network call requires.
Three preparations make that true rather than aspirational: keep the module’s interface narrow enough to be a plausible API, avoid transactions that span two modules, and keep an eye on which modules call which — a module everything depends on is not extractable at any price.
How it fails
Two failure modes account for most disappointment, and both are visible early if anyone looks.
Modules that are only a naming convention. Nothing prevents a cross-module call into an internal class, so under deadline pressure the boundaries erode invisibly and the result is a conventional monolith with aspirational folder names. The tell is that no build step would fail if someone violated a boundary — if no check exists, assume the boundaries are already gone.
Shared tables. The modules look separate in the code and share a schema underneath, so a change to one module’s storage breaks another. This is the same coupling that makes a distributed system disappointing, and here it is cheaper to fix — which is the argument for fixing it now rather than after extraction.
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.
Discover more from Insightful Data Lab
Subscribe to get the latest posts sent to your email.
