There's a pitch that's very hard for an ambitious engineer to resist. Split the app into microservices. Make it event-driven. Let each service scale and deploy independently. Draw the architecture diagram with a dozen boxes and arrows, and it looks like a real company built it. The trouble is that distributed architecture is not free, and the bill doesn't arrive all at once. It arrives as a complexity tax — a compounding cost you pay on every feature, every deploy, every 2 a.m. incident, forever. Plenty of teams sign up to pay it years before they have the scale that would justify it, largely because the simple thing felt embarrassing. This is a vendor-neutral look at what that tax actually costs, why teams overpay it early, when it's genuinely worth paying, and why a boring modular monolith wins for far longer than the architecture diagrams admit.
What the Complexity Tax Actually Is
The seductive thing about microservices is that each service, looked at alone, is simpler than the monolith it replaced. That's true, and it's also the trap — because the complexity doesn't vanish. It moves. It relocates from inside a process, where your compiler, your debugger, and your transactions can all see it, to the spaces between processes, where none of them can.
Concretely, the moment you split one app into several services talking over a network, you trade:
- A function call for a network call. What was instant and reliable is now slow and fallible — every cross-service call can time out, fail partway, or retry, and you have to handle each of those cases that an in-process call gave you for free.
- A local transaction for a distributed one. “Save the order and decrement inventory” was one atomic commit. Across two services it becomes a saga, a two-phase dance, or — most often — eventual consistency and the reconciliation logic that comes with it.
- One deploy for many. And with many independently deployable services comes versioning, backward-compatible contracts, and the question of what happens when service A ships before service B.
- One log stream for a haystack. A request that used to be one stack trace is now a journey across five services, which is why you suddenly need distributed tracing, correlation IDs, and a real observability budget just to answer “what happened to this request?”
None of these is unsolvable. That's the point — each is solvable, and the sum of all that solving is the tax. You're now running a distributed system, with all of its famous fallacies in play, to ship the same features you shipped before.
Why Teams Overpay It Early
If the tax is so steep, why do small teams keep volunteering for it? The reasons are mostly cultural, not technical:
- Resume-driven development. Microservices, Kafka, and a service mesh look better in a postmortem talk and on a CV than “we kept it in one Rails app.” The incentives of the engineer and the needs of the product quietly diverge.
- Cargo-culting the giants. Netflix, Uber, and Google went distributed for reasons that are very real at their scale. Copying their architecture without their problems is copying the cast on a broken leg you don't have.
- The seriousness reflex. A monolith feels like a prototype; a constellation of services feels like a company. That feeling is doing a lot of architectural decision-making, and it shouldn't be.
- Splitting before there are teams to split. The strongest case for service boundaries is letting independent teams own independent pieces — but if you have three engineers, you've drawn network borders through code that all three of you edit every week.
Microservices Are an Org Chart, Not a Performance Trick
Here's the reframe that dissolves most premature-microservices decisions: the architecture that lets a 5,000-engineer company ship is solving an organizational problem — how do hundreds of teams deploy without tripping over each other — far more than a technical one. Conway's Law cuts both ways: services that mirror real team boundaries buy autonomy, but services drawn through a small team's shared work just add network calls between things that change together. If you're adopting microservices and you can't name the teams each service belongs to, you're buying the solution to a problem you don't have yet.
When the Tax Is Worth Paying
This is emphatically not an argument that distributed systems are wrong — they're indispensable at the right scale. The question is whether you have a real forcing function, not an aspiration. The honest ones:
- Team scale. When you have enough engineers that one shared codebase and one deploy pipeline is genuinely the bottleneck, carving services along team boundaries buys independent deployability — the actual, durable microservices win. This is the big one, and it's organizational.
- Genuinely divergent scaling. When one component must scale on a different curve or different hardware — a GPU-bound inference path, a firehose ingest endpoint, a CPU-heavy report generator — extract that piece so it can scale alone. You rarely need to distribute the whole app to isolate the one hot part.
- Fault isolation. When a critical path must not be taken down by a noisy neighbor, a process boundary is a blast wall. That's a real, specific reason — for that path.
- Hard boundaries of change or compliance. A component with a wildly different release cadence, or one that must live inside a regulated boundary, can earn its own service on those grounds alone.
Notice what every one of these has in common: it names a specific service and a specific reason. “We might need to scale someday” is not on the list, because it isn't a forcing function — it's a guess, and you're paying interest on it now for a payoff that may never come.
Why the Boring Monolith Wins Early
The alternative to premature distribution isn't the naive big-ball-of-mud monolith everyone learned to fear. It's the modular monolith: a single deployable unit, organized internally into clear modules with boundaries you actually enforce — the same separation of concerns microservices give you, minus the network in the middle.
You keep everything the tax would have taken: one deploy, one log stream, a debugger that sees the whole request, real database transactions, and in-process calls that can't time out. And you keep the option value, which is the part people miss. Well-drawn module boundaries are exactly the seams you'd later extract a service along — so if a genuine forcing function does show up, you carve that one module out, and by then you'll actually know where the seam belongs instead of having guessed at it on day one. A clean schema and clear module boundaries — the kind of discipline our guide to designing a multi-tenant database schema walks through — is what makes that future extraction a refactor rather than a rewrite. The modular monolith isn't the timid choice. It's the one that keeps the most doors open for the longest time.
The Decision, Honestly
So make it on purpose. Default to the monolith, and add a service the moment — but only the moment — you can name the forcing function it solves: a team that needs to deploy on its own clock, a component that must scale or fail independently, a boundary the business actually requires. Anything short of that, and you're letting the architecture diagram make a decision the product hasn't earned.
The asymmetry is what settles it. Starting with a monolith and extracting a service later is a well-understood, incremental refactor. Starting distributed and trying to consolidate a sprawl of premature services back into something comprehensible is one of the most miserable projects in our field — ask anyone who has done it. This is the same instinct behind the broader unbundling cycle in developer tooling and behind why small teams outship funded ones: a lot of speed comes not from the sophisticated thing you added, but from the expensive complexity you had the discipline not to buy. Pay the complexity tax when the value is real. Until then, keep the money.
A Monolith You Won't Outgrow Too Soon
ShipKit is a production-ready FastAPI backend built as a clean, modular monolith — clear boundaries, real transactions, one deploy — so you start simple and extract services later, only if you ever truly need to.
Explore ShipKit