# Monolith vs Microservices, Straight

> What a monolith and microservices each actually are, the real strengths and real costs of both, and a clear way to choose instead of cargo-culting the architecture everyone's blogging about.


---

# Monolith vs Microservices, Straight

There's a meeting you've probably sat in. Someone says "we should move to microservices," and the room nods, because microservices are what real companies do - and nobody wants to be the person defending the boring old monolith. A few quarters later, the same team is drowning in network timeouts, half-finished services, and a deploy process that needs a spreadsheet to coordinate.

This is the most over-argued choice in software architecture, and most of the arguing skips the part that matters: *what each one actually is*, and *what it actually costs you*. This guide walks both sides fairly - no slurs, no hype. By the end you'll be able to reason about the decision for your own team instead of copying someone else's blog post.

## How to read this
- **Already leaning one way and want the clear-eyed counter-case?** Skim [Phase 1](01-the-monolith.md) and [Phase 2](02-microservices.md) for the side you're *not* championing - that's where the surprises are.
- **Want it to finally make sense?** Read in order. Phase 1 builds the mental model of a monolith, Phase 2 builds microservices as a response to its limits, and Phase 3 gives you a way to actually choose.

## The phases
1. **[The Monolith](01-the-monolith.md)** - one deployable application: what it actually is, where it genuinely shines, and where it starts to strain.
2. **[Microservices](02-microservices.md)** - many small independently-deployed services: the strengths, and the costs people consistently underplay.
3. **[How to Actually Choose](03-how-to-actually-choose.md)** - judgment, flagged as judgment: how to read your team's real pain and decide, plus the two traps that catch everyone.

> This guide is about the *shape* of your system, not how to operate one once you've chosen. The deep mechanics of scaling a single service, and the messaging glue that holds services together, live in their own guides - linked at the end of each phase.


---

# The Monolith

"Monolith" gets said with a little sneer, like a confession - *we're still on a monolith*. That feeling does you a disservice. Most successful software you've ever used started as a monolith, and a great deal of it still is one. Before you can fairly weigh anything against it, you need to see what it really is, on its own terms.

## What a monolith actually is

**What it actually is.** A monolith is **one application that you build, deploy, and run as a single unit**. All your features - login, billing, search, the admin panel - live in one codebase and ship together as one artifact (one server process, one container image, one deployable). They call each other by calling functions, in the same process, sharing the same memory and usually the same database.

**Why people get this wrong.** The common wrong picture is "monolith = messy, tangled, big ball of mud." That's a description of *bad code*, not of a monolith. A monolith can be beautifully organized into modules with clean boundaries - it's still a monolith, because it deploys as one thing. The defining trait is the deployment unit, not the tidiness.

Here's the whole shape in one picture:

```mermaid
flowchart TD
  subgraph App["ONE APPLICATION (deploys & scales as one unit)"]
    direction TB
    Login[Login]
    Billing[Billing]
    Search[Search]
    DB[(DB)]
    Login --> DB
    Billing --> DB
    Search --> DB
  end
```

## Where the monolith genuinely shines

This is the part the sneer hides. A monolith has real, durable advantages - not "good enough for now" ones.

**It's simple to build, deploy, and run.** One repo to clone, one command to start, one thing to deploy. A new hire is running the whole system locally on their first morning - no service-discovery layer, no inter-service auth, no "which of the seventeen services owns this?" When you deploy, you deploy *the app*: one artifact, one version.

**Debugging is one stack trace.** When something breaks, the entire request lives in one process. You set a breakpoint, you step through it, you read one log stream from top to bottom.

**A real example.** A request fails, and the trace shows you the whole path, end to end:
```console
$ tail -n 8 app.log
ERROR  POST /checkout  500
  at chargeCard (billing/charge.js:42)
  at placeOrder (orders/place.js:88)
  at handler (routes/checkout.js:15)
NullError: card.token is undefined
```
*What just happened:* billing and orders run in the same process, so the failure is one continuous stack trace from the HTTP handler down to the exact line - `placeOrder` called `chargeCard` with a card that had no token. No correlation IDs, no jumping between dashboards; the whole story is in one place.

**Transactions are easy.** This one is underrated. When billing and orders share a database, "charge the card *and* save the order, or do neither" is a single database transaction. The database guarantees it for you.
```console
$ psql -c "BEGIN; INSERT INTO orders ...; UPDATE accounts SET balance ...; COMMIT;"
COMMIT
```
*What just happened:* both writes happened inside one transaction. If either failed, the database rolls back both - you can never end up with a charged card and no order. This atomic-across-features guarantee is nearly free in a monolith and something you have to fight hard for once features live in separate services (see [Phase 2](02-microservices.md)).

**Refactoring across features is safe.** Renaming a function that billing and orders both use? Your compiler or test suite catches every caller, because they're all in one codebase. Change a shared model and everything that depends on it updates together.

## Where the monolith starts to strain

A monolith isn't free of limits - it's just that the limits show up later and for more specific reasons than the hype suggests. There are two that genuinely bite.

**One big team on one codebase.** A handful of developers in one repo is a pleasure. Fifty developers in one repo, all merging into the same `main`, all needing to deploy on their own schedule, is friction. Every deploy ships *everyone's* changes, so one team's risky feature can block another team's urgent fix. Coordination cost grows with the number of people sharing the deployment unit.

**Scaling the whole thing to scale one part.** A monolith scales by running more copies of the *entire* application behind a load balancer. That works fine - until one slice has wildly different needs from the rest.

Your image-processing endpoint is CPU-hungry. Everything else is light. To give image-processing more CPU, you must run more copies of the WHOLE app:

```mermaid
flowchart LR
  C1["Copy 1<br/>login · billing · images (CPU) · search"]
  C2["Copy 2<br/>login · billing · images (CPU) · search"]
  C3["Copy 3<br/>login · billing · images (CPU) · search"]
```

*The strain:* you can't give the image-processing code more resources without also duplicating login, billing, and search alongside it. Often that's perfectly acceptable - copies of a stateless app are cheap. It becomes a real problem only when one part's resource appetite is so different from the rest that duplicating everything to feed it is genuinely wasteful.

**The gotcha - don't mistake messy code for "monolith problems."** When a monolith feels painful, the cause is usually tangled internal boundaries, not the fact that it's a monolith. A monolith with clean module boundaries (sometimes called a *modular monolith*) keeps almost all the simplicity while staying easy to reason about. Splitting a tangled monolith into services doesn't untangle it - it spreads the tangle across a network, which is worse. (More on that trap in [Phase 3](03-how-to-actually-choose.md).)

> 📝 **Modular monolith** - a monolith deliberately organized into well-separated internal modules with clear interfaces, so it stays one deployable but doesn't become a big ball of mud. It's the strong default this guide keeps coming back to.

⚠️ **"Monolith" is not a slur.** It's an architecture with a specific, often excellent, set of trade-offs. Plenty of large, busy products run happily on a well-built monolith for years. The real question is never "are we still a monolith?" - it's "are we feeling a *specific* pain that a different shape would actually fix?"

## Recap

1. A **monolith** is one application built, deployed, and run as a single unit; features call each other as in-process function calls.
2. "Monolith" describes the **deployment unit**, not the code quality - a clean modular monolith is still a monolith.
3. Its real strengths: **simple to build/deploy/run, one stack trace to debug, easy database transactions, safe cross-feature refactoring.**
4. It strains when **one big team shares one deploy** and when **one part needs to scale very differently from the rest.**
5. Most "monolith pain" is **tangled code, not the monolith itself** - and splitting tangled code into services makes it worse.

With the monolith seen fairly, you're ready to look at the architecture built specifically to relieve those two strains - and to pay for that relief in new ways.


---

# Microservices

If the monolith strains when one big team shares one deploy and one part needs different scaling, the obvious move is to *split it up*. That's what microservices are: a direct answer to those two strains. The answer works, and it arrives with a bill the brochures tend to leave off. Both halves, plainly stated.

## What microservices actually are

**What it actually is.** Microservices are **many small applications, each built, deployed, scaled, and owned independently**, that talk to each other over the network. Instead of one process with a billing module, you have a *billing service* - its own codebase, its own deploy, often its own database - that other services call over HTTP or a message queue.

**Why people get this wrong.** The wrong picture is "microservices are just a monolith chopped into folders." The defining change isn't the chopping - it's that **the calls between pieces are now network calls between separately-deployed programs**, not function calls in one process. That single shift is the source of every benefit *and* every cost on this page. Hold onto it.

Here is the same checkout system from Phase 1, now as services:

*Monolith - one deploy, one shared database, in-process calls:*
```mermaid
flowchart LR
  Modules["login · billing · search (one app)"] --> MonoDB[(one database)]
```
*Microservices - each its own deploy and database, network calls between:*
```mermaid
flowchart LR
  Login[login svc] --> LDB[(DB)]
  Billing[billing svc] --> BDB[(DB)]
  Search[search svc] --> SDB[(DB)]
```

The lines that used to be free function calls inside one box are now arrows crossing a network between boxes. Everything that follows comes from that.

## Where microservices genuinely shine

These are real wins, and they map directly onto the monolith's two strains plus one bonus.

**Independent scaling.** That CPU-hungry image-processing endpoint from Phase 1? As its own service, you run more copies of *just it* - and leave login and billing at one copy each. You pay for the resources the hungry part needs, and nothing else.

```console
$ kubectl scale deployment image-service --replicas=10
deployment.apps/image-service scaled
$ kubectl get deployments
NAME            READY
image-service   10/10
billing-service 1/1
login-service   1/1
```
*What just happened:* you scaled only the image service to ten copies; billing and login stayed at one each. This is the headline benefit - resources go exactly where the load is, instead of duplicating the whole app to feed one hungry slice.

**Independent deploys and team autonomy.** Each service ships on its own schedule. The billing team can deploy ten times a day without touching the search team's code or release, and a risky change in one service can't block an urgent fix in another, because they're different artifacts. For a large organization, this is often the *real* reason to adopt microservices - an org-structure win as much as a technical one.

**Fault isolation.** If the search service crashes, it crashes alone. The rest of the system can keep serving - checkout still works, login still works - as long as you've designed the callers to tolerate search being down. In a monolith, a memory leak in one module can take down the whole process; here the blast radius is one service.

## The costs people underplay

This is the half that gets skipped, and it's where teams get hurt. None of these are dealbreakers - they're the price of admission, and you should know the price before you buy.

**Network calls everywhere.** Every arrow in that diagram is now a network round-trip that can be slow, can time out, or can fail entirely while the rest keeps running. A function call in a monolith either returns or throws. A network call has a third outcome the monolith never had: *no answer at all.*

```console
$ curl http://billing-service/charge
curl: (28) Operation timed out after 30000 milliseconds
```
*What just happened:* the order service asked billing to charge a card and got... nothing - not success, not a clean failure, just silence. Did the charge go through? You genuinely don't know. Every service-to-service call needs timeouts, retries, and a plan for "I'm not sure what happened" - code and complexity that doesn't exist in a monolith.

**Distributed debugging.** The single clean stack trace from Phase 1 is gone. One user request now hops through five services, each with its own logs, on its own machine. To reconstruct what happened you need to stitch those logs together, which means every request must carry a shared **correlation ID** so you can find all its pieces.

> 📝 **Correlation ID** - a unique value attached to a request when it enters the system and passed along to every service it touches, so you can search all the scattered logs for that one ID and reassemble the request's full journey. In a monolith you never needed one; across services it's mandatory.

```console
$ grep "req-9f2a1c7" logs-from-all-services/*.log
gateway.log:  req-9f2a1c7  POST /checkout  -> order-service
order.log:    req-9f2a1c7  calling billing-service
billing.log:  req-9f2a1c7  charge OK
order.log:    req-9f2a1c7  calling inventory-service
inventory.log:req-9f2a1c7  TIMEOUT
```
*What just happened:* you searched every service's logs for one correlation ID and reassembled the request by hand. The failure was inventory timing out - but finding that took grepping five log files instead of reading one stack trace. Doable with good tooling (distributed tracing), but real work you now have to build and maintain.

**Data consistency across services.** This is the deepest cost, and the one that quietly wrecks projects. In a monolith, "charge the card *and* save the order, or do neither" was one database transaction (Phase 1). When billing and orders are separate services with separate databases, **no single transaction can span both.** You can charge the card, then have the order save fail - and now reality is inconsistent.

```text
   MONOLITH                      MICROSERVICES
   one DB, one transaction       two DBs, NO shared transaction

   BEGIN                         billing-svc: charge card    ✓ done
     charge card                 order-svc:  save order      ✗ failed
     save order                  ──────────────────────────────────
   COMMIT  (both or neither)     card charged, no order - inconsistent!
                                 you must detect and undo this yourself
```

Fixing this means giving up the database's automatic guarantee and building your own: patterns like *sagas* (a sequence of steps each with a compensating "undo"), or making operations safe to retry, or accepting that the system is only *eventually* consistent. All of that is design and code you didn't need before.

**Operational overhead.** A monolith is one thing to deploy and watch. Microservices are many, and you now also own the *spaces between them*: service discovery (how does order-service find billing-service?), inter-service authentication, a CI/CD pipeline per service, monitoring per service, and often a whole orchestration platform to run it all. This is real, ongoing staffing cost - a common rule of thumb is that you shouldn't adopt microservices until you can comfortably operate the platform they require, though exactly where that line falls is judgment, not a measured number.

⚠️ **The costs are not optional add-ons.** Network failure handling, correlation IDs, cross-service consistency, and the ops platform aren't "nice to haves you'll get to later." They're load-bearing. Skip them and you don't get microservices - you get an *unreliable* monolith spread across a network, which has every cost on this page and none of the benefits. ([Phase 3](03-how-to-actually-choose.md) names that trap directly.)

## Recap

1. **Microservices** are many small applications, each independently deployed and owned, talking over the **network** - that network boundary is the source of everything that follows.
2. Real strengths: **independent scaling** (feed only the hungry part), **independent deploys + team autonomy**, and **fault isolation**.
3. Underplayed costs: **network calls can vanish silently**, **debugging is now distributed** (correlation IDs, tracing), **no transaction spans services** (you build consistency yourself), and **ops overhead multiplies**.
4. Those costs are **mandatory**, not optional - skipping them gives you the worst of both worlds.

You've now seen both architectures fairly, with their wins and their bills laid side by side. The last phase is the one that actually helps: how to choose for *your* team, and the two traps that catch people who choose for the wrong reasons.

> The "talk over the network" glue - message queues, event-driven communication, and how services stay loosely coupled - is its own topic. See [Webhooks and Message Queues](/guides/webhooks-and-message-queues). For how to scale any single service well (which a monolith needs too), see [Designing for Scale](/guides/designing-for-scale).


---

# How to Actually Choose

Everything so far has been fact: what each architecture *is* and what it *costs*. This phase is different - it's **judgment**, flagged as judgment so you can weigh it against your own situation rather than treat it as law. Reasonable, experienced engineers disagree at the edges here. What follows is the position most battle-scarred practitioners land on, and the reasoning behind it, so you can decide for yourself.

## The decision cheat-card

> **In a meeting and need a position right now? Start here, then read the reasoning below.**

| Your situation | The straight default |
|---|---|
| New product, small team, still finding fit | **Start with a (well-structured) monolith** (§1) |
| Monolith works fine, no specific pain | **Stay** - "we're a monolith" is not a problem (§1) |
| One specific part needs very different scaling | **Split out that one service** - not everything (§2) |
| One team is blocked by everyone else's deploys | **Split along that team's boundary** (§2) |
| "We should do microservices because that's what real companies do" | ⚠️ **Stop** - that's not a reason (§3) |
| Tangled monolith you want to fix by splitting | ⚠️ **Untangle first** - splitting spreads the mess (§3) |

## 1. The default: start with a well-structured monolith

**The judgment.** *For most teams, most of the time, the right starting point is a well-structured monolith - ideally a modular one.* This is opinion, but it's opinion grounded in the costs you read in Phase 2.

Here's the reasoning. Early on, the things that kill a product are not scaling limits - they're shipping slowly, building the wrong thing, and running out of runway. A monolith optimizes for exactly the things you need then: fast iteration, easy debugging, cheap refactoring, one thing to deploy. The microservices bill from Phase 2 - network failure handling, distributed debugging, cross-service consistency, an ops platform - is a tax you'd be paying *before* you have the problems it buys relief from.

And critically: a monolith is not a one-way door. If you build it with clean internal module boundaries, those boundaries are the natural seams you'll cut along *later*, when and if a real reason appears.

💡 **Key point.** The strong default isn't "monolith forever." It's "monolith *first*, with clean internal seams, so that splitting later is cheap when a specific reason shows up." You buy the option to split without paying for it up front.

## 2. Split out a service when you feel a *specific* pain

**The judgment.** *Don't migrate to microservices as a project. Split out one service when a named, concrete pain makes the cost worth it - and split only that part.* "Microservices" is rarely the right goal; "relieve this specific pain" is.

What does a real reason look like? It's specific enough to point at:

- **"The image-processing endpoint needs ten times the CPU of everything else, and duplicating the whole app to feed it is genuinely wasteful."** That's a scaling reason - split out the image service, leave the rest alone.
- **"The payments team can't ship a fix without coordinating a release with four other teams, and it's costing us real velocity."** That's a team-autonomy reason - split along that team's boundary so they own their own deploy.
- **"The reporting jobs are heavy and occasionally take the whole app down with them."** That's a fault-isolation reason - move reporting to its own service so its problems stay contained.

Notice the shape: each one names a *part*, names the *pain*, and the fix is to extract *that part* - not to rewrite the system. You can run a mostly-monolith with two or three services peeled off where it actually helps. That hybrid is not a failure to commit; it's often the wisest end state.

**A real example of the disciplined move:**
```console
$ git log --oneline --grep="extract" -3
a1b2c3d  Extract image-processing into its own service (CPU isolation)
9f8e7d6  Add correlation IDs across app + image service
4c5b6a7  Define image-service API contract, keep monolith as caller
```
*What just happened:* The team didn't "go microservices." They extracted exactly one service for one stated reason (CPU isolation), and - crucially - they added correlation IDs and a clear API contract *as part of the same work*, because those are the mandatory costs from Phase 2. One pain, one split, costs paid in full.

## 3. The two traps that catch everyone

These are where good teams get hurt. Both come from choosing for the wrong reason.

### Trap 1 - The distributed monolith

⚠️ **The distributed monolith: all the coupling of a monolith, none of the benefits of microservices.** This is the worst of both worlds, and it's distressingly common. It happens when you split a system into services *but the services are still tightly coupled* - they have to be deployed together, they share a database, or every change to one forces changes to the others.

It looks like microservices, but acts like a monolith - you can't deploy one service without the others, they share one database, and one change ripples through all three:

```mermaid
flowchart TD
  A[svc A] --> B[svc B] --> C[svc C]
  A --> DB[(one shared DB)]
  B --> DB
  C --> DB
```

You're paying the full microservices bill - network calls, distributed debugging, ops overhead - and getting none of the rewards, because the services can't actually move independently. **The tell:** if deploying one service routinely requires deploying others in lockstep, or they all read and write the same tables, you have a distributed monolith. The cure is real boundaries: each service owns its own data and can deploy on its own. If you can't give a candidate service those things, it shouldn't be a separate service yet.

### Trap 2 - Premature splitting

⚠️ **Premature splitting: drawing service boundaries before you understand the domain.** Early on, you don't yet know where the natural seams in your system are - which parts truly change together and which are independent. Carve the system into services too early and you'll almost certainly draw the lines in the wrong places. Moving a boundary *after* it's a network boundary is brutally expensive: changing API contracts, migrating data between databases, coordinating across teams - instead of the simple cross-module refactor it would have been inside a monolith (Phase 1).

The deeper irony: splitting a *tangled* monolith into services to "clean it up" doesn't untangle it. It takes the tangle and stretches it across a network, turning every messy function call into a messy network call. **Untangle the code first, inside the monolith, where refactoring is cheap and safe. Find the real seams. Then - if a specific pain still calls for it - split along seams you've actually verified.**

## Recap (all judgment, flagged as such)

1. **Default to a well-structured (modular) monolith** for most teams - it optimizes for the things that matter early, and keeps clean seams for later.
2. **Don't migrate to microservices as a goal.** Split out a service when a **specific, named pain** (scaling, team autonomy, fault isolation) makes the Phase 2 costs worth paying - and split only that part.
3. **A hybrid** - a monolith with a few services peeled off where they help - is often the wisest end state, not a half-measure.
4. **Avoid the distributed monolith** (coupled services = all the costs, none of the benefits) and **premature splitting** (wrong boundaries, drawn before you understand the domain).
5. The recurring real question is never "are we modern enough?" - it's **"are we feeling a specific pain that a different shape would actually fix?"**

That's the whole decision, told fairly: two real architectures, each with genuine strengths and a clear-eyed bill, and a way to choose based on your team's actual pain instead of the loudest voice in the meeting.

> To go deeper on the pieces this guide touched: [What "Architecture" Means](/guides/what-architecture-means) for the foundations, [Designing for Scale](/guides/designing-for-scale) for scaling any service well, and [Webhooks and Message Queues](/guides/webhooks-and-message-queues) for the communication glue between services.

Watch it animated: [monolith vs. microservices](/explainers/MonolithMicroservices.dc.html)
