The Monolith Isn't Dead, You're Just Holding It Wrong
A defense of the single deployable from someone who has cleaned up after both kinds of mess: what monoliths actually buy you, the mud-vs-monolith distinction, and the scale arguments dismantled one by one.
Every few months a startup founder shows me their architecture diagram, and it has eleven services, two message brokers, a service mesh, and four users. Total. Four. Somewhere along the way we collectively decided that a single well-organized codebase is something to be embarrassed about, and I'd like to spend the next thousand words pushing back on that, because the most profitable systems I've ever worked on were monoliths — and the most expensive messes were distributed systems built by teams of five.
What a monolith actually buys you
Let's be precise, because "monolith" gets used as an insult for "big ball of mud", and those are different things. A monolith is one deployable unit. That's all. And one deployable unit comes with superpowers that distributed systems spend millions trying to reimplement:
- Transactions.
BEGIN … COMMITacross your whole domain. Order, inventory, payment record — one atomic operation. In microservices, this becomes a saga with compensation logic, and I promise you the compensation logic has bugs, because nobody tests the failure paths as hard as the database tests atomicity. - Refactoring that works. Rename a method and the IDE updates 200 call sites in four seconds. Try renaming a field in an event that three other teams consume. Now you have a versioning committee.
- One debugging surface. A stack trace tells the whole story. No correlation IDs across six services, no "the trace sampled out exactly the request that failed".
- Deployment that's boring. One artifact, one rollback. Your CI pipeline is fifteen lines.
These aren't consolation prizes. They're the reason a three-person team on a monolith regularly outships a fifteen-person team on microservices.
The actual disease (and it's not "monolith")
The systems people flee from aren't bad because they're monoliths. They're bad because they have no internal boundaries: the invoice module reaches into the user module's tables, everything imports everything, and one change ripples everywhere. That's a big ball of mud, and here's the uncomfortable truth — a team that builds a mud monolith will build mud microservices. Except now the mud has network calls in it, and the coupling that used to be a compile error is a runtime 500 at 2 a.m.
The fix is boundaries, and boundaries don't require networks. In Laravel that might look like:
app/
├── Modules/
│ ├── Billing/ # its own models, services, events — and a facade
│ │ ├── BillingApi.php ← the ONLY class other modules may touch
│ │ └── Internal/ ← everything else, enforced by convention or tooling
│ ├── Catalog/
│ └── Identity/
Other modules call BillingApi, never Internal\InvoiceRepository. Enforce it with an architecture test (Pest's arch() tests or Deptrac in PHP, ArchUnit in .NET/Java):
// tests/Architecture/ModuleBoundariesTest.php
arch('catalog does not reach into billing internals')
->expect('App\Modules\Catalog')
->not->toUse('App\Modules\Billing\Internal');
That single failing test in CI does more for your architecture than any diagram. I've written more about this shape in the modular monolith piece — it's the destination most teams should actually aim for.
"But it won't scale"
Scale what, exactly? Let's separate the three things people mean:
- Traffic. Monoliths scale horizontally just fine — run ten copies behind a load balancer. Shopify handles Black Friday on (a very sophisticated) monolith. Stack Overflow ran one of the highest-traffic sites on Earth on a handful of servers. Your bottleneck at scale is almost always the database, and splitting the app into services does nothing for the database — that's what caching, read replicas and indexing are for.
- Team. This one's real. Thirty engineers merging into one deploy pipeline steps on each other. But the honest threshold is much higher than people think — with good modules and trunk-based development, a monolith comfortably serves several teams. The pain that justifies extraction is organizational, and it arrives around "multiple teams blocked on each other's release schedules", not around "we hired a fifth developer".
- Hot spots. One endpoint needs 50× the compute? Extract that one thing. A monolith plus two deliberately extracted services is a great architecture. It's also how every successful "microservices" story actually happened — gradually, from a working monolith, along seams the monolith revealed. Nobody credible designed 40 services on a whiteboard first.
My defaults, stated plainly
- New product, team under ~10: modular monolith, one database, no exceptions I've yet regretted.
- Extract a service only when you can name the specific, current pain — a scaling hot spot, a genuinely different runtime need, a team-ownership boundary — and the pain is worth an on-call rotation, a contract to version, and a tracing setup.
- Never extract to make the org chart look modern. Conway's Law collects its debt either way.
The monolith isn't the architecture you settle for. Done with discipline, it's the architecture that lets a small team punch far above its weight — and the best possible launchpad if you genuinely outgrow it. The sequel to this post covers exactly when that happens.
Inherited a codebase where everything touches everything? Carving boundaries into a live monolith is delicate, satisfying work — and precisely the kind I take on.