The Pragmatic Monolith
We've talked three separate clients out of a microservices rewrite this year. Not because microservices are wrong — because none of them had the problem microservices solve yet.
"We need to break this up into microservices" is one of the most common requests we get from growing engineering teams. It's also, more often than not, the wrong next step — not because microservices don't work, but because the problem being described usually isn't a service-boundary problem at all.
The actual problem, most of the time
When we dig into "we need microservices," the real complaint is almost always one of three things: deploys are slow and risky, the codebase is hard to navigate, or one team's changes keep breaking another team's code. All three are real problems. None of them require distributed systems to fix.
Splitting a tangled monolith into services doesn't untangle the dependencies — it just moves them over the network, where they're harder to see and much harder to debug.
What actually fixes each complaint
- Slow, risky deploys → usually a build/test pipeline problem, fixable with better caching, parallelization, and progressive delivery (see our piece on that) — not a deployment-topology problem.
- Hard to navigate codebase → usually a module-boundary problem inside the monolith. Enforcing clear internal package boundaries with dependency-direction rules gets most of the benefit people expect from service boundaries, without the network hop.
- Teams breaking each other's code → usually an ownership and interface-contract problem, solvable with clearer module ownership and contract tests between internal packages.
// Enforcing an internal boundary without a network boundary
// eslint rule shape: packages/billing cannot import from packages/reporting
{
"rules": {
"boundaries/no-unknown": "error",
"boundaries/element-types": [
"error",
{ "rules": [{ "from": "billing", "disallow": ["reporting"] }] }
]
}
}
When microservices are actually the answer
We do recommend splitting a system when the constraint is genuinely about independent scaling (one component needs ten times the infrastructure of the rest), independent deployment cadence at real organizational scale (dozens of teams, not three), or a hard isolation requirement (compliance boundary, a genuinely separate failure domain). Those are real, common-enough reasons — they're just less common than the number of teams asking for a rewrite.
"Microservices are a tool for organizational scaling problems. Most teams asking for them still have a code-organization problem, and code organization is fixable without a network hop."
Internal module boundaries enforced inside a single deployable monolith
What we tell clients instead
Enforce internal module boundaries first, with real tooling, not just convention. If that genuinely isn't enough — if the constraint really is independent scaling or deployment cadence at organizational scale — the boundaries you've already enforced internally become the seams you extract along. The rewrite gets easier, not harder, by doing this first.
One email, every other Friday
Engineering notes, new articles, and things we learned the hard way — no marketing, ever.
Read by 2,400+ engineers. Unsubscribe anytime.
Let's talk
Let's build what's next.
If any of this sounds like the kind of work you want done on your systems, our engineers are the ones who'll actually do it.
