Monolith
A monolith is one application, built and deployed as a single unit. The word is used as an insult and should not be: a monolith with real module boundaries is the cheapest architecture that works, and it is the correct default for most systems. What ruins monoliths is not their size but the absence of internal boundaries — and that is a different problem with a different fix.
- Deploy unit
- One artefact
- Call cost
- Microseconds
- Failure mode
- Eroded boundaries
What the word actually means
A monolith is an application whose code is built, tested and deployed as one unit. That is the whole definition. It says nothing about how big the codebase is, how many people work on it, whether the code is well organised, or whether it is any good.
The confusion comes from a second meaning that attached itself to the word around 2014: the big ball of mud, where everything calls everything, no piece can be understood alone, and a change to invoicing breaks the login page. That is a real and common condition. It is just not caused by single-unit deployment, and it is not cured by splitting the deployment.
The version worth aiming at has a name: the modular monolith. Inside one deployable, the code is divided into modules with explicit interfaces — orders, billing, catalogue, users. A module may call another module only through its published interface, never by reaching into its data. Deployment stays simple; the internal structure is what a set of services would have, minus the network.
Why this is the right default
The case for starting monolithic is not nostalgia. It is that a single process removes an entire category of problems that you would otherwise have to solve before writing any business logic.
- A call between modules is a function call: microseconds, no serialisation, no timeout, no retry, and it either happened or it did not.
- A transaction across two modules is one database transaction. No sagas, no compensating actions, no eventual consistency to explain to the business.
- Moving a boundary is a refactor the compiler checks. In a distributed system the same change is a migration across two repositories, two deploys and a versioning question.
- A stack trace covers the whole request. Debugging is reading; it does not require correlating traces across four services.
- One thing to run locally. A new engineer is productive on day one rather than in week two.
The deeper argument, and the one Fowler made under the name "monolith first", is about knowledge. Service boundaries are expensive to move and you get them wrong when you do not yet understand the domain — which, at the start of a product, you do not. A monolith lets you move boundaries cheaply while you are still learning where they belong, and defers the expensive commitment until you have evidence for it.
How to keep the boundaries
Boundaries in a monolith are not enforced by anything unless you make them so. Nothing stops a developer importing a class from another module at two in the morning, and nothing will tell them it was wrong. In a distributed system the network enforces the boundary by being inconvenient; in a monolith you have to supply that inconvenience deliberately.
| Mechanism | What it prevents | Cost |
|---|---|---|
| Module per build unit | Importing internals — only what the module exports is visible to others. | A slower build graph |
| Architecture tests | Any dependency the rules forbid, caught in CI with the offending line named. | A day to set up, then free |
| Schema per module | Cross-module joins and foreign keys — the change that is hardest to undo later. | Some queries become two queries |
| Ownership in code review | Boundary changes slipping in unnoticed inside an unrelated pull request. | One more reviewer sometimes |
The one with the best return is architecture tests. A dozen lines that assert "nothing outside the billing module may import billing.internal" turn a rule everyone agrees with and nobody enforces into a build failure with a name on it. Without them, module boundaries survive exactly as long as the person who cares about them stays on the team.
Where a monolith genuinely runs out
There are real limits, and being honest about them is what separates an argument from a preference. None of them is about code size.
- One scaling unit. If image processing needs sixteen times the CPU of everything else, you scale the whole application sixteen times to get it.
- One runtime and one language version. A module that would be far better in a different stack cannot have one.
- One release cadence. Twelve teams deploying one artefact means twelve teams coordinating, and the slowest one sets the pace.
- One blast radius. A memory leak in a rarely used report takes down checkout, because they share a process.
- Build and test time, which grows with the codebase and eventually makes the merge-to-production loop slow enough to change behaviour.
Read that list as a diagnostic rather than a verdict. Each item has a cheaper answer before the architectural one: the CPU-hungry module can be extracted on its own without splitting anything else, build time responds to test parallelisation and module-level caching, and release contention is often a branching and review problem rather than a deployment one. Extract the one service that solves the actual constraint; that is not the same decision as adopting microservices.
Monolith vs. microservices
Modular monolith
- Boundaries enforced by tooling you must add
- Calls are functions — no partial failure
- One transaction spans modules
- Moving a boundary is a refactor
- Scales as one unit, in one stack
Microservices
- Boundaries enforced by the network, unavoidably
- Calls fail halfway — every one needs a policy
- Cross-service consistency is a saga
- Moving a boundary is a migration
- Scales and evolves per service
The trade is the same one in every row: microservices buy independence and pay for it in distributed-systems work. That work does not appear on any plan, it is never finished, and it is the reason a team of six is almost always faster on a monolith. Buy independence when you have something to spend it on — several teams that genuinely block each other, or one component whose scaling profile is nothing like the rest.
How this shows up in real delivery
The most common expensive mistake in this area is not choosing a monolith or choosing microservices. It is choosing microservices to fix a modularity problem. The team is slow because everything is coupled, the diagnosis is "we need to split this up", and the split happens along the coupling that already exists — producing services that must be deployed together, share a database, and now also fail over the network. That result has a name: the distributed monolith, and it is strictly worse than what it replaced.
Where it degrades
- Joins across module tables, which couple two modules through storage rather than through an interface.
- Layered packages — controllers, services, repositories — which group by technical role and give every feature the whole codebase as its surface.
- A shared "common" or "utils" module that everything depends on and nobody owns.
- Module rules agreed in a document rather than asserted in CI, so they last one quarter.
- A test suite slow enough that people stop running it locally, which removes the fast feedback that made the monolith cheap.
When to use it
Use it when
- The product is new and the domain boundaries are still being learned.
- One team, or a few teams that can coordinate a release without pain.
- Transactional consistency across features matters and eventual consistency would be a business problem.
- You want to spend your complexity budget on the domain rather than on infrastructure.
- You are prepared to enforce module boundaries in the build rather than in a document.
Avoid it when
- Many teams genuinely block each other on one release train and the coordination cost is measured.
- One component has a scaling profile nothing like the rest and must scale independently.
- Parts of the system have genuinely different availability or compliance requirements.
- A component would be far better in another stack and the case for it is technical, not fashionable.
Found this useful?
Share it with someone who is working on the same problem.