Monolith vs. microservices — the actual trade-off

"Microservices are the modern, scalable choice" is the popular framing, and it's wrong often enough to be worth correcting directly — this is a trade-off with real costs on both sides, not a straightforward upgrade.

Intermediate

5 min read

What each term actually means

A monolith is a single deployable application — one codebase, one build, one process (or a small number of identical replicas of it) that contains all of an application's functionality: user accounts, orders, payments, notifications, all running as one unit, typically talking to one shared database. Microservices split that same functionality into multiple independent, separately deployable services — an orders service, a payments service, a notifications service — each with its own codebase, its own deployment, often its own database, communicating over the network (usually HTTP APIs, or the message queues from an earlier lesson).

What a monolith actually costs you at scale — the real argument for splitting

One shared codebase, one shared deploy:
- A bug in the notifications code can crash the entire process, taking down orders and payments too
- Every team's changes go through the same deploy pipeline, so one team's slow test suite blocks everyone
- The whole application scales as one unit — even if only the orders code is under heavy load,
  every replica carries the full weight of every other feature too

These are the real, legitimate problems microservices solve: a fault in one service doesn't (by default) take down unrelated services running in a different process; teams can deploy independently, on their own schedule, without coordinating a shared release; and each service scales independently — running 20 replicas of just the orders service under heavy load, while notifications stays at 2, instead of scaling the entire monolith as a single unit regardless of which specific piece is actually under load.

What microservices cost you — the part that's easy to skip past

Every one of these arrows is now a network call — something that used to be a plain in-process function call is now genuinely slower, can time out, can fail independently of the caller, and needs the retry/idempotency handling covered in earlier lessons. A single logical operation ("place an order") that used to be one function call inside one process now involves multiple services, multiple network hops, and multiple independent failure points — any one of which failing has to be handled explicitly, where a monolith's in-process call either just works or the whole request fails together, atomically, with none of this coordination needed.

Distributed transactions become genuinely hard. A monolith wraps "charge the card AND create the order" in one database transaction — it either both happens or neither does, guaranteed by the database. Once payments and orders are separate services with separate databases, there's no single transaction spanning both — this is exactly the cross-shard transaction problem from the sharding lesson, showing up again for a different reason, requiring the same kind of harder pattern (sagas, compensating actions) instead of a plain COMMIT.

Operational complexity multiplies. A monolith is one thing to deploy, one thing to monitor, one set of logs to search through. Ten microservices are ten things to deploy, ten sets of logs scattered across ten services (which is why distributed tracing exists — to reconstruct one logical request's path across many services after the fact), and a genuinely harder debugging experience: "why did this request fail" now might mean checking five different services' logs instead of one stack trace in one place.

The size where the trade-off actually flips

For a small team and a young product, a monolith is very often the right choice, not a compromise to eventually grow out of: one codebase is simpler to reason about, easier to refactor across (renaming something used in three "services" is one PR instead of three coordinated deploys), and doesn't require the operational maturity — service discovery, distributed tracing, an API gateway, independent CI/CD pipelines per service — that microservices genuinely need to run well. Splitting too early adds all of microservices' costs (network calls, harder transactions, more operational surface) before the team is large enough to actually benefit from independent deployability, since a five-person team deploying one monolith isn't actually blocked by shared deploys the way a 200-person org with 15 teams is.

The realistic middle ground: a modular monolith

# One deployable process, but internally organized as if it were separate services:
myapp/
  orders/       # its own models, views, internal API — doesn't import payments internals directly
  payments/
  notifications/

A modular monolith — one codebase and one deployment, but with clear internal boundaries between modules, each with its own models and a defined internal interface other modules go through, rather than reaching into each other's internals directly — gets much of microservices' organizational clarity (each module owns its own concerns, has clear boundaries) without paying the network-call and distributed-transaction cost, since it's still one process with in-process function calls under the hood. This is frequently the actual right starting point, with a path to extracting a genuinely justified module into a real separate service later — once "this specific piece needs independent scaling/deployment" is demonstrated by real need, not assumed upfront.

The practical question, restated

The decision isn't "which is more modern" — it's "does this specific problem (independent scaling of one component, independent team deploy schedules, fault isolation between unrelated features) actually exist yet, at a scale that's worth the real network/transaction/operational cost of solving it with microservices?" Most systems, most of the time, don't have that problem yet when they're first built — which is exactly why well-known companies (Shopify, GitHub, Stack Overflow, for long stretches of their growth) ran successful monoliths at large scale, splitting out specific services only once a specific, demonstrated need justified the real cost of doing so.

Further reading

Check your understanding

A quick comprehension check — not tracked, not graded, just for you.

1. What real problem does splitting a monolith into microservices solve, according to this lesson?

2. What does a plain in-process function call become once it crosses a service boundary in a microservices architecture?

3. Why is 'charge the card AND create the order' harder to guarantee correctly in microservices than in a monolith?

4. According to this lesson, what does a modular monolith get you that a plain monolith doesn't, without paying microservices' full network/transaction cost?