The Monolith
One deployable, in-process calls, real transactions across the whole domain — and costs that arrive at team boundaries, not at request volume.
The requirement, the obvious build, and why it breaks
Every lesson starts where the work starts: someone asked for something, and the first implementation that comes to mind has a problem.
What does a single deployable actually give you, and what specifically takes it away?
A team of six is building a product that will change shape several times before anyone knows what it is. They have to choose a structure now that will not have to be undone.
One codebase, one process, one database. Anything can call anything, a request handler can reach into any table, and a feature is a pull request rather than a coordination problem.
Nothing, for a long time. That is the point of this lesson: the naive version is right for longer than most teams believe, and the costs below arrive on a schedule set by team growth rather than by traffic.
- Nothing, for a long time. That is the point of this lesson: the naive version is right for longer than most teams believe, and the costs below arrive on a schedule set by team growth rather than by traffic.
- When it does break, it breaks structurally: after two years everything imports everything, so a change to invoicing touches the checkout tests, and nobody can say what depends on what.
- One process means one failure domain. A memory leak in report generation kills checkout (Memory Leaks in Backend Services); CPU-bound PDF rendering stalls every other in-flight request on a single-threaded runtime (Blocking the Event Loop).
- One deploy pipeline means deploy contention: at five teams, releases queue behind each other, and one team's rollback reverts another team's feature.
- One resource profile: the whole thing scales together, so a component that needs a lot of memory forces every instance to have it (Horizontal vs Vertical Scaling).
What is actually happening
- A monolith's real advantage is that a call is a call. There is no partial failure, no serialization, no timeout, no version skew, no retry semantics. The function either returns or throws, in the same process, in microseconds.
- It has one transaction boundary available across the entire domain. Writing an order, decrementing stock and recording a ledger entry can be genuinely atomic, with the database enforcing it. That option disappears permanently the moment those tables live behind different services (Where the Transaction Boundary Goes, The Dual Write Problem).
- It has one place to look. One log stream, one stack trace across the whole call path, one profiler run covering the whole request, one repository to search.
- Refactoring is compiler-checked. Renaming a concept or changing a signature across the entire system is one atomic change that tools verify. Across services, the same change is a versioned contract migration with two deployed versions running at once (Expand and Contract Migrations).
- Scaling a monolith horizontally is entirely normal: run many identical stateless instances behind a load balancer. "Monolith" describes the deployable, not the instance count, and this is the confusion that produces most of the bad advice about it (Making an Existing Service Stateless).
What one process actually buys
It is worth being concrete about the advantages, because they are usually described as "simplicity" and then dismissed. They are not vague. Each one is a specific capability that becomes unavailable, permanently, the moment the components are separated.
The transaction row is the heaviest. Once two writes are in different services, atomicity across them is gone and you are choosing between a saga, an outbox and eventual consistency — all of which are real engineering, none of which is as correct as a database transaction (The Transactional Outbox, Saga Pattern in Software Architecture).
| Capability | In one process | Across services | What the split costs you |
|---|---|---|---|
| Calling another component | A function call: returns or throws | A network request: returns, throws, or neither | Partial failure becomes a case in every call site (Failure Propagation) |
| Atomicity across components | One database transaction | Not available | Saga or outbox, plus compensating logic and a consistency window |
| Refactoring a shared concept | One atomic, tool-verified change | A versioned contract migration across two live versions | Expand/contract and a coordination window (Expand and Contract Migrations) |
| Debugging one request | One stack trace | A distributed trace, if you built one | Tracing becomes infrastructure you must operate (Tracing From the Backend's Side) |
| Deploying a change | One artifact | N artifacts with compatibility rules between them | Version skew as a permanent condition |
| Local development | Run the process | Run the graph, or mock most of it | Onboarding and test fidelity both get worse |
| Scaling one component | Not independently | Independently | This is the thing you gain, and the only one on this list |
The costs arrive on a headcount schedule
The strain in a monolith does not correlate with request rate. It correlates with the number of people who need to change it at once and with how entangled it has been allowed to become. That is why "we are getting big" is not a signal and "three teams are waiting on the deploy queue" is.
The trigger column below is the useful part. Each of these is measurable, and each has a response that is cheaper than a split — which is the response to try first.
| Trigger | Symptom | Cause | Response |
|---|---|---|---|
| Three teams share one release pipeline | Releases queue; one rollback reverts unrelated work | Deploy contention — the genuine trigger for splitting | Feature flags and trunk-based deploys first; extract a service only if contention persists (Feature Flags: Rollout, Kill Switches and Debt) |
| Report generation runs in the request path | p99 on unrelated endpoints rises whenever reports run | One shared pool and one shared runtime | Move to background workers from the same codebase; separate pools (Background Jobs, Bulkheads) |
| Everything imports everything | A small change breaks distant tests; nobody can scope a refactor | No enforced internal boundaries | Build-enforced modules inside the monolith (The Modular Monolith) |
| One component needs 30 GB of memory or a GPU | Every instance is sized for the outlier and costs accordingly | A genuinely divergent resource profile | Extract that one component — this is a real reason, and it justifies exactly one service |
| Test suite passes 30 minutes | Deploy frequency falls; people batch changes | Suite scope grew with the codebase | Parallelise and tier the suite by module; splitting the service does not make the tests faster |
| A memory leak takes down checkout | A whole-system outage from an unrelated feature | One failure domain | Bulkheads, limits and canaries; accept that full isolation requires a separate process |
One deployable, many instances
The single most common misunderstanding is that a monolith is one running process. It is one *artifact*. Run twenty of them behind a load balancer, run some of them as queue consumers with the HTTP server disabled, and you have independent scaling of workloads without a single distributed call.
This shape — one codebase, several deployment roles — covers a large fraction of what teams believe they need services for, and it keeps every in-process advantage from the first section.
How to build it
Most important first.
- Start here unless you have a specific, named force that requires otherwise. The forces are real and they are listed under
scalebelow — "we will be big one day" is not one of them. - Keep the deployable single and put the boundaries inside it: modules with public interfaces and private data, enforced by the build (The Modular Monolith). This is where most of the perceived benefit of services is actually available.
- Scale it horizontally and keep it stateless, so capacity is an instance count rather than a rewrite (Stateless Services, Horizontal vs Vertical Scaling).
- Move work that does not belong in the request path into background workers built from the same codebase and deployed separately. This gives independent scaling and failure isolation for the expensive part without splitting the domain (Background Jobs, Worker Scaling).
- Isolate resources inside the process: separate connection pools per workload, bounded concurrency per dependency, so the report query cannot consume the checkout pool (Bulkheads, Connection Pools).
- When you do extract a service, extract one, along a boundary that already exists in the code, for a reason you can state in a sentence (Microservices).
What can go wrong
- Boundaries that exist only in folder names. Without build-level enforcement, the modular structure erodes within about a year and nobody notices until an extraction is attempted.
- One slow endpoint saturating the shared worker or connection pool and taking down endpoints that have nothing to do with it (Failure Propagation).
- A test suite that grows superlinearly until nobody runs it locally, which is the point at which deploy frequency starts to fall.
- A shared database schema where every module reads every table, so the coupling is in the data model and no amount of code tidiness reaches it.
- Startup time growing until rolling deploys and autoscaling become sluggish, which shows up as a capacity problem during a traffic spike (Startup Time & Cold Start in Cloud & Infrastructure).
- Everything in a monolith's process is shared by every in-flight request: module-level caches, singletons, connection pools (Backend Races).
- Multiple instances of the same monolith reintroduce every distributed concern for shared state — an in-process lock is not a lock any more (A Mutex on Server A Does Nothing About Server B in Concurrency & Parallelism).
- One process means one blast radius: any code in the monolith can reach every credential in its environment and every table its database user can see. Least privilege has to be enforced at the module level in code, because the process boundary is not doing it (Defence in Depth).
- One advantage in the other direction: authentication and authorization are enforced once, in one pipeline, with no internal network hops that might be assumed to be trusted (The Trust Boundary).
- A monolith is markedly easier to audit. There is one place where requests enter, one place where secrets are configured, and one dependency tree to keep patched (Dependency Security).
- "Monolith means legacy." It means one deployable. A monolith written this year with enforced modules is a modern architecture and often the correct one (The Modular Monolith).
- "Monoliths cannot scale." They scale horizontally like anything else stateless. What does not scale is a monolith holding local state, which is a statelessness problem rather than an architecture one (Stateless Services).
- "We are big, so we need microservices." Size is not the criterion; team boundaries, deploy contention and resource-profile divergence are. §119 is explicit about this and the mistake is still the most common in this module (Microservices).
- "Splitting will fix our coupling." Coupling that exists in the code becomes coupling over a network, with latency, partial failure and version skew added. Modules first; services later, if at all.
- "One database means one schema for everyone." A monolith can have per-module schemas with separate database users, and that is a good idea long before an extraction is on the table.
Operating it
- Endpoint-level latency and error rates by route, plus a shared-resource view: pool utilisation, queue depth, worker saturation. In a monolith the shared resources are where cross-endpoint interference appears (Saturation: The Reading Utilization Cannot Give You in Observability & Performance).
- Track deploy frequency and time-from-merge-to-production per team. Rising numbers with a flat headcount are the signal that deploy contention has begun — the actual trigger for splitting, and one nobody watches.
- Track test-suite duration alongside them. It is usually the first of the two to move.
- Attribute resource use by module — queries per module, allocations per module — so "which part is expensive" is answerable before someone proposes extracting the wrong one.
- At 10x traffic: usually nothing changes. Add instances. This is the answer that gets skipped and it is right far more often than it is given.
- What actually forces a split is not traffic. It is deploy contention (multiple teams blocking on one pipeline), a genuinely divergent resource profile (a GPU workload, something needing 30 GB of memory), a compliance or isolation boundary, a required runtime or language the monolith cannot host, or blast-radius isolation for a critical path that must survive the rest.
- At 100x, a large monolith is still viable and several very large systems run this way. What is not viable is a large monolith with no internal boundaries, because at that size nobody can predict the effect of a change.
- One failure domain: a leak, an infinite loop or a bad deploy affects everything. Mitigate with bulkheads and canary deploys, and accept that mitigation is not isolation (Canary Deployments).
- One release cadence: everything ships together, so the slowest reviewer sets the pace once the team is large enough for that to bind.
- One runtime: a workload that would be far better served by another language has to be tolerated or moved out.
- The cost of *not* splitting is paid slowly and is easy to ignore; the cost of splitting is paid immediately and is easy to see. That asymmetry is why teams both split too early and stay too long.
Where this applies
Backend advice is context-sensitive. These labels say what each claim is specific to, and where a different stack or scale would differ.
- SCALE-SPECIFICBelow roughly three teams sharing one deploy pipeline, a monolith is almost always the right default and the costs listed here are theoretical. Above that, deploy contention becomes a measurable delay per release and the calculus changes — traffic volume does not change it at any point.
- GENERALThe in-process advantages — real transactions, no partial failure, compiler-checked refactoring, one stack trace — hold in every language and runtime. They are properties of a shared address space, not of any stack.
Where the depth lives
This domain teaches the application-side mechanism and hands the rest off.
- — System Design — the interview framing of this decision, where "start with a monolith" is usually the strongest available answer and is usually skipped.