"Microservices Were a Mistake for Most Companies" has been making the rounds again, and it's largely right. Most teams that split their product into thirty services never had thirty teams, thirty independent scaling problems, or thirty real failure domains — they had one product, one roadmap, and a conference talk that made the org chart look aspirational. The sprawl that follows is real: nobody can point to where a piece of business logic actually lives, the same validation rule gets copy-pasted into six repos because a shared library turned into its own coordination tax, and an incident that used to mean "check the logs" now means tracing a request across a dozen services owned by three teams, one of which doesn't exist anymore. That diagnosis isn't wrong. What's wrong is the cure it usually implies — collapse everything back into one deployable — because that doesn't fix the underlying problem. It just moves it somewhere worse.

What the sprawl critique gets right

The core complaint holds up under scrutiny. A service per verb, a service per noun, a service because a blog post said so — that's not architecture, it's an org chart cosplaying as one, usually copied from a company two hundred times the size with a genuinely different scaling problem. Every one of those boundaries has a real, ongoing cost: a separate repo to keep patched, a separate CI pipeline to keep green, a separate on-call rotation nobody has time to actually staff. Multiply that by thirty services and a twelve-person engineering team spends more time maintaining the seams between services than building the product those services are supposed to add up to.

The duplication problem compounds it. Auth checks, retry logic, the same three lines of input validation — copied into every service because a shared library across a dozen independently-deployed codebases becomes its own versioning nightmare, and a network call to a shared "utility service" for something that small adds latency to fix a problem a function call never had. What was supposed to be separation of concerns turns into the same concern, badly separated, maintained in a dozen places by people who don't know the other eleven exist.

What "just go back to a monolith" quietly re-introduces

The instinct to collapse it all is understandable, and for a small team it's often correct at the start. But treating "one deployable" as the fix for sprawl trades a diffuse failure mode for a concentrated one, unless someone deliberately builds in the isolation a monolith doesn't give you for free. A memory leak in the reporting module can take down checkout, because by default they're the same process. A bad regex pegs a CPU core, and the whole app — not just the page that ran it — stops responding to everyone, unless something upstream was deliberately put there to stop it: a separate worker pool, a bulkhead, a circuit breaker. Most monoliths don't have that by default. Without it, the blast radius isn't smaller than microservices — it's just always sized to the whole system, for every bug, regardless of how unrelated the code that caused it was to the code that goes down.

Deploys get harder to coordinate as headcount grows, not easier. One team's Friday schema migration blocks everyone else's release, because there's only one release. A dependency bump one module needs forces every other module to compile against it, whether that team asked for the upgrade or not. The coordination tax that microservices sprawl put between services, a monolith puts between people — same tax, paid in code review queues and release calendars instead of network hops.

And the scaling coupling is real. A batch export job, an image-processing pipeline, a search index — anything with a resource profile ten times heavier than the rest of the request path — forces the whole app to scale to feed that one hot path, because there's no way to scale just the part that's actually under load. You end up paying for idle compute everywhere else in the system to keep one corner of it fed.

A monolith doesn't automatically give you a smaller blast radius. Without deliberate isolation, unrelated failures can share the same fate simply because they share the same runtime and deployment boundary.
Same system, three different ways to draw the lines One Monolith One process, one datastore, one release ONE BUG TAKES DOWN EVERYTHING Microservices Sprawl 40 REPOS, NO ONE OWNS THE MAP Right-Sized Services Payments Catalog Core App Exports BOUNDARIES MATCH OWNERSHIP
Neither extreme draws the line around anything real. A single deployable makes every bug a company-wide outage; thirty services makes every incident a scavenger hunt across repos nobody fully owns. A handful of services drawn around actual ownership and actual fault domains gets the isolation that matters without paying for isolation that doesn't.

The real variable is blast radius and ownership, not a service count

The question was never how many services you should have. It's: when this breaks, who does it take down with it, and who is actually on the hook to fix it at 2 a.m.? A service boundary earns its keep only when it clears one of three tests — it isolates a fault domain that genuinely can't be allowed to take the rest of the system down with it, it lets a component with a real, different scaling profile scale on its own, or it lets a team that owns something end-to-end ship it on their own schedule without waiting in someone else's PR queue. Fail all three, and a boundary is paperwork with a network hop bolted into the middle of it.

The Three Tests for a Service Boundary

  • Fault — does this need to fail independently, without taking the rest of the system down with it?
  • Scale — does this need to scale independently, on a resource profile nothing else on the request path shares?
  • Ownership — does a specific, staffed team need to ship this independently, on its own schedule?

If a boundary doesn't buy you at least one of those, it's probably just another repo.

If you can't draw an org chart with a real, separately-staffed team on each side of a service boundary, you don't have two services. You have one service with a slow network call in the middle of it.

That's also just Conway's Law working as designed, not against you. Service boundaries drawn to match the team boundaries a company actually has tend to hold up, because the people who own a service are the same people who feel the pain when its boundary is wrong. Boundaries drawn to match the team structure a company wishes it had — the one from the conference talk, the one it'll grow into in three years — don't hold up, because there's no team on the other side to feel that pain and fix it. Eight engineers do not have a microservices-at-scale problem. They have a "who owns the checkout button" problem, and forty services makes that harder to answer, not easier.

A working rule of thumb: a handful, not one and not forty

My rule of thumb, after twenty-five years of watching this go both ways: if you're under roughly fifty engineers, I want a very specific reason before your independently deployable service count leaves the single digits — not one, and not dozens. A boundary earns that reason when it's true today, not hypothetically, that something needs to be isolated for compliance or blast-radius reasons — payment card data, PHI — that it has a genuinely different resource or scaling profile from the rest of the app, or that a specific, staffed team owns it end-to-end and needs to release on its own cadence. Everything else stays in one codebase, because everything else doesn't actually have a reason to be anywhere else yet.

That codebase doesn't have to be a mess to stay unified — it has to be modular. A "modular monolith" enforces boundaries between its own internal pieces at the build and import-graph level instead of over a network: a payments module can't reach directly into the orders table, a reporting module can't call into checkout's internals, and the compiler catches the violation instead of an incident report catching it. You get most of the readability and ownership benefit people actually wanted from microservices, without paying a network-call tax or duplicating auth and validation logic six times. And when a piece of it genuinely does need to become its own service later, the boundary's already drawn — you're changing where it deploys, not discovering for the first time under pressure where the seam should have been.

How to tell a real boundary from organizational cosplay

Before splitting anything out, it's worth running it through three honest questions.

  • Does this split actually change who can deploy independently — or does it just add a network call between two things one team still owns?
  • If this service went down for an hour, would the rest of the system be fine — or does everything downstream break immediately too?
  • Is there a second team, staffed and on call today, who would actually own this — not a team you could hire for it eventually?

If the honest answer to all three is no, the split is solving an org chart the company doesn't have yet, and it'll cost more in coordination overhead than it saves in isolation. That's exactly the sprawl the original critique is describing — just moved earlier, before the incident that would have made the cost obvious.

What this looks like in practice

In practice, a company usually earns its second and third services in a predictable order. Everything starts in one modular monolith while the product is still finding its shape — the same stage where a working prototype still needs someone to design for the failure modes a demo never hits. The first real split is usually something with a genuinely different risk profile — payments, or a module handling regulated data, where the case for isolation is the same one that shows up in what a HIPAA-compliant architecture actually has to separate — not because it needs to scale differently, but because it needs to fail differently. The second is usually something with a different resource profile: a background job queue, an export pipeline, a search index. Everything else — the actual product — stays together until a specific team, for a specific reason, needs it apart. For a fifty-person engineering org, that's often four or five services. Not one. Not forty.

Where this leaves you

The "microservices were a mistake" critique is right that sprawl is expensive and that most companies paid for isolation they never needed. It's wrong to conclude the fix is zero services. The fix is drawing boundaries around the failure domains, scaling needs, and team ownership a company actually has — not the count on either side of the debate.

Microservices weren't the mistake. The mistake was treating a deployment model as an architecture.

A service boundary should represent something real: a failure boundary, a scaling boundary, or an ownership boundary. If it represents none of those things, it's probably just another repo.

Joseph Rounds

Founder, Lighthouse Consulting

25+ years building enterprise software at McKesson (Fortune 10), Doctor On Demand, and IntelyCare. Now helping Boston-area businesses design and build custom software, AWS infrastructure, and AI integrations that fit how they actually operate.