Microservices Were Never About Scale
Teams split monoliths for throughput and got an org-chart fix. The honest case is deploy cadence and ownership — plus narrow cases where boundaries help load.
5 min read
On this page
The stated reason for splitting a monolith is almost always performance. The system is slow, the database is struggling, one endpoint times out under load, and the plan that emerges from the room is a set of services. Eighteen months later the endpoint is still slow, there are now nine deployables, and nobody wants to say out loud that the original problem was an unindexed query.
The strange part is that the split often was worth doing. It just wasn't worth doing for the reason given.
The usual argument, stated properly
The case for services on scaling grounds is not stupid, and it deserves better than a strawman.
If your image processing pipeline and your login endpoint live in the same process, they share a deployment unit, a memory limit and a thread pool. To give the pipeline more capacity you must replicate the login endpoint too, including its connection pool, its warm caches and its licence cost. Splitting them lets you scale each along its own axis and pay only for what you need. It also gives you failure isolation: a memory leak in report generation shouldn't take down the checkout path.
That reasoning is sound. It's also a much narrower claim than the one people make when they say "we're going microservices to scale".
What actually got fixed
Ask a team that completed a migration what improved, and the honest answers are rarely about latency.
They can deploy without a release train. Two teams can ship on the same afternoon without coordinating a merge window. A change to billing no longer requires a full regression suite over search. Ownership is legible — when the recommendation service pages someone, everyone knows who. New engineers can hold one service in their head instead of a codebase that took a year to learn.
Every one of those is a fix for a coordination problem. The binding constraint was not the machine; it was the number of people who had to agree before anything shipped. Services solve that by turning a social contract into a network boundary, and it genuinely works, because a compiler will not let you cheat across an API the way you can cheat across a module.
This is why the performance framing survives. "Our release process cannot absorb more engineers" is an uncomfortable thing to write in a design document. "We need to scale" is respectable, requires no admission about how the organisation works, and gets funded.
The cheaper thing you skipped
The tell is what was never tried. Before the split, was there a query plan anyone had actually read? A cache in front of the read path that dominates traffic? A background job for the synchronous work that didn't need to be synchronous? An instance one size up?
Vertical scaling is deeply unfashionable and remarkably effective. A single machine today holds an amount of memory that would have been a small cluster not long ago, and the operational cost of using it is roughly zero — no service discovery, no distributed tracing to explain a latency regression, no partial failure, no schema coordination across teams, no saga to replace the transaction you used to get for free.
The comparison worth making is not "monolith versus services". It's "the next six months spent on service extraction versus the next six months spent on the three slowest queries and a bigger box". For a large share of teams the second option wins by an embarrassing margin, and it's reversible.
Where boundaries genuinely do help load
There are real cases, and they share a shape: the workloads differ in kind, not just in volume.
Divergent resource profiles. A video transcoder wants CPU and ephemeral disk. An API wants memory and connections. A machine learning inference path wants a GPU you don't want idling behind your health check endpoint. When two workloads want different hardware, sharing a deployment unit means overprovisioning both.
Isolating a hot path. If one endpoint carries the overwhelming majority of traffic and everything else is incidental, pulling it out lets you tune it aggressively — its own connection pool, its own cache, its own autoscaling policy, its own on-call rota — without dragging the rest of the application through every change.
Different elasticity. Traffic that spikes by an order of magnitude for an hour a day should be able to scale to near zero the rest of the time. Traffic that is flat should not be paying for that machinery.
Different failure tolerance. A path that must stay up when the database is degraded needs a much smaller dependency set than a path that can wait. Making that boundary explicit is easier when it's a separate process.
Notice that none of these are "the system is slow". They're all "these two things want different treatment".
The question to ask instead
Before the next split, run one thought experiment. Suppose you could deploy the monolith safely ten times a day, with independent ownership of each module enforced by tooling, and no team blocking another. Would you still want to break it apart?
If the answer is yes, you have a genuine resource or isolation argument and you should go ahead — the boundary is buying you something a bigger machine cannot.
If the answer is no, then what you needed was a deployment pipeline, module boundaries with teeth, and a conversation about ownership. Distributed systems are a very expensive way to enforce a code review policy.