all series

systems design

the monolith

not an antipattern. the default that has real advantages worth understanding.

“we split it into microservices. deploys now take four hours and we created three new job titles to manage the complexity. worth it.”
anonymous architect, 2019

"monolith" has drifted into a pejorative. something legacy, something you graduate from. that framing treats a deployment topology as a maturity level, which is how teams end up paying distributed systems costs for problems they do not have.

a monolith is a system deployed as one unit. the code runs in one process, or a set of identical processes behind a load balancer. calls between modules are function calls. database work happens on one connection, in one transaction.

what it is not is unstructured. a good monolith has module boundaries, encapsulated data access, and clear ownership internally. the thing that distinguishes it from a distributed system is where the network sits, not whether the code is organized.

what you get for free

each of these is something a distributed system has to rebuild, usually badly at first.

calls cannot fail halfway. a function call from the order module to the inventory module either returns or throws. it does not time out, arrive twice, or succeed while the response is lost. every one of the ambiguities in partial failure simply does not exist inside a process, and the code you do not have to write is the retry logic, the idempotency keys, the circuit breakers, and the reconciliation jobs.

transactions work. one database, one connection, BEGIN and COMMIT across as many tables as you like. atomicity is a property of the tool rather than a distributed protocol you implement and get subtly wrong.

latency is microseconds. an in-process call is essentially free next to a network round trip. an operation touching six modules is fine; the same operation across six services is a latency budget problem and a tail latency problem.

debugging is one stack trace. one process, one log stream, one place to attach a profiler. the distributed equivalent is correlating IDs across services and reconstructing a timeline, which needs infrastructure you have to build and maintain before the first incident.

deploys are atomic. one artifact ships and one artifact rolls back. there is no window in which version N and version N+1 of two services disagree about a message schema.

where it genuinely hurts

the real limits are narrower than the discourse suggests, and they are mostly about people and cadence rather than technology.

scaling is all-or-nothing: if image processing needs more CPU, you scale everything, and you pay for capacity the rest of the system does not use. deployment is coupled, so a risky change in a rarely-touched module blocks a queue of unrelated ready changes, and the pain here is proportional to deploy frequency, not codebase size. the runtime is shared, so adopting a better tool for one problem means a whole-system migration or a service anyway. and past some team size a single codebase becomes a coordination surface: merge conflicts, calcifying shared abstractions, a test suite slow enough that people stop running it.

notice that three of those four are functions of team size and deploy frequency. a monolith with four engineers deploying weekly has none of these problems and will not develop them by getting older. the same codebase with 200 engineers deploying hourly has all of them.

when the trade flips

decomposition earns its cost when something has genuinely diverged:

resource profiles diverge. one component needs GPUs or 20x the memory, and coarse scaling is now a real line item rather than a rounding error.

deploy cadences diverge. teams are blocking each other often enough that coordination is a measurable tax.

runtime requirements diverge. a component is genuinely better served by a different language or database, not just a more interesting one.

team boundaries are real. you have distinct teams with distinct product ownership. this is the one that actually predicts success, because a service boundary that does not match a team boundary produces the operational cost of a service and the coordination cost of a monolith at the same time. conway's law is not a warning here so much as a design input.

and the reasons that do not hold up: the codebase feels messy (refactor it. a mess distributed across services is the same mess with network calls in it), microservices are what serious companies do, someone wants to use a new technology, or it will be faster (it is almost always slower; you added network hops to operations that were function calls).

start here, and mean it

martin fowler's monolith first argues the empirical case: the successful microservice systems he observed almost all started as monoliths that were later split, and the ones built as microservices from scratch mostly ran into trouble. the reason is straightforward. at the start you do not yet know where the boundaries are, and a boundary in the wrong place is far more expensive to move once it is a network interface with independent deploys behind it.

so the useful investment early is module boundaries inside the monolith. they are cheap to draw, cheap to move when you get them wrong, and they are exactly what you extract along later if the trade flips. keep the modules honest and defer the network boundary until staying monolithic actually costs more than the distributed system would.

what to do when it does flip is service decomposition .