Backend / Architecture & design / 08_monolith_vs_microservices.md

Monolith vs microservices

Updated 5 interview angles 5 min read source
On this page9
  1. The two models
  2. Comparison
  3. When the monolith is the right call
  4. When microservices start to pay off
  5. The middle ground: modular monolith
  6. The microservices tax
  7. Migrating monolith → microservices
  8. Common confusions
  9. Interview angle

Monolith vs microservices

Two ways to deploy a system. A monolith is one deployable unit — one codebase, one process, one database. Microservices split the system into many independently deployable services, each owning its data, communicating over the network.

The honest answer to “which is better” is: start with a monolith; split when a concrete pain forces you to. Microservices solve organizational and scaling problems at the cost of distributed-systems complexity. If you don’t have those problems, you’re paying the cost for nothing.

The two models

text
Monolith                          Microservices
┌───────────────────┐             ┌────────┐ ┌────────┐ ┌────────┐
│ orders            │             │ orders │ │ users  │ │ billing│
│ users     (1 proc)│             └───┬────┘ └───┬────┘ └───┬────┘
│ billing           │                 │  network │ (HTTP/   │
│                   │                 │          │  events) │
│   one database    │             ┌───┴───┐  ┌───┴───┐  ┌───┴───┐
└───────────────────┘             │  DB   │  │  DB   │  │  DB   │
                                  └───────┘  └───────┘  └───────┘
in-process function calls         network calls + own DB per service
ACID transaction across all       no distributed transaction

Comparison

Dimension Monolith Microservices
Deployment one unit many independent units
Cross-module call in-process function call network call (latency, can fail)
Data consistency ACID transaction across all data eventual; sagas + compensations
Scaling scale the whole app scale hot services independently
Team autonomy shared codebase, coordinated releases teams own + deploy services independently
Tech stack one (mostly) per-service choice
Failure isolation a bug can take down the process one service can fail in isolation
Local dev / debugging run + step through one process run many services, distributed traces
Operational cost low high (CI/CD, observability, networking)

When the monolith is the right call

  • Small team, early product, requirements still moving.
  • Strong consistency matters and a single ACID transaction is the simplest path.
  • You can’t yet operate a broker, service mesh, distributed tracing, and per-service CI/CD.
  • The system isn’t big enough that any one part needs independent scaling.

Most products should be here longer than they think. A monolith is not a mistake to apologize for.

When microservices start to pay off

  • Team scale — many teams stepping on each other in one codebase; independent deploys remove the coordination tax. (This is the primary driver — it’s an org solution as much as a technical one.)
  • Divergent scaling — one component (e.g. video transcoding) needs 50x the resources of the rest; scale it alone.
  • Independent release cadence / fault isolation — a risky service can deploy and fail without taking the rest down.
  • Heterogeneous tech — a part genuinely needs a different language/runtime.

The middle ground: modular monolith

Before jumping to services, get the boundaries right inside one deployable: enforce module separation (each module owns its tables, talks to others only through explicit interfaces), no cross-module DB reads. You get clean boundaries and independent reasoning with none of the network tax — and if you later need to split, well-bounded modules become services cleanly. This is the recommended default for most growing systems. See Clean Architecture and Domain-Driven Design (DDD) for drawing those boundaries.

The microservices tax

Splitting turns in-process calls into network calls, and that changes everything:

  • No distributed transactions — consistency becomes eventual; you need sagas + compensating actions (Data Consistency Across Services) and the outbox pattern (Transactional Outbox Pattern).
  • Partial failure is normal — every call can time out; you need retries, timeouts, circuit breakers.
  • Observability is mandatory — a request spans services; without distributed tracing + correlation ids you’re blind (Distributed Tracing).
  • Operational overhead — per-service CI/CD, versioned APIs, schema evolution, service discovery, possibly a service mesh.

Move much of the inter-service coupling onto events to keep services autonomous — see Event-Driven Architecture and Event-Driven Microservices.

Migrating monolith → microservices

Don’t rewrite. Use the strangler fig pattern: put a routing layer (API gateway) in front, carve out one bounded capability at a time into a service, route its traffic there, repeat. The monolith shrinks until it’s gone or is just another service.

The routing layer is the whole mechanism, and it is boring on purpose:

nginx
location /api/billing/ {
    proxy_pass http://billing-service;   # extracted
}

location / {
    proxy_pass http://monolith;          # everything else
}

One prefix moves at a time. Each move is independently revertible — change the line back — which is what makes this safe where a rewrite is not.

Splitting the database is the hard part. Extracting the tables is easy; the cross-module joins are not:

sql
-- Before: one query, one database.
SELECT o.*, u.email FROM orders o JOIN users u ON u.id = o.user_id;

After the split that join does not exist. The options are an API call per order (N+1 over the network), a batch lookup, or replicating email into the orders service via an event and accepting it is seconds stale. There is no option that keeps the join, and choosing between those three is the actual design work in a migration — see Data Consistency Across Services.

Common confusions

  • “Microservices are faster.” — usually slower per request (network hops). They help scaling and team velocity, not single-request latency.
  • “Microservices = better architecture.” — only if you have the problems they solve. Otherwise you get a distributed monolith: services so chatty and coupled they must deploy together — all the cost, none of the benefit.
  • “Small services are the goal.” — the goal is the right boundaries (one business capability), not the smallest possible services. Too-fine splitting multiplies the network/ops tax.

Interview angle 5

  • “Monolith or microservices — which would you choose for a new project?” — start with a (modular) monolith; split only when a concrete pain appears (team coordination, divergent scaling, fault isolation). Don’t pay distributed-systems cost without the problem.
  • “What’s the biggest hidden cost of microservices?” — losing ACID across the system: every multi-step operation becomes eventual consistency with sagas and compensations, plus the need for tracing, retries, and per-service ops.
  • “What is a distributed monolith and why is it bad?” — services split physically but still tightly coupled (synchronous chains, shared DB, lockstep deploys). You get the network/ops overhead of microservices with the coupling of a monolith.
  • “How do you migrate a monolith to microservices?” — strangler fig: route through a gateway, peel off one bounded capability at a time, and split its data last (the hardest part). Never a big-bang rewrite.
  • “What’s a modular monolith?” — one deployable with strictly separated modules (own tables, interface-only access). Gives clean boundaries without the network tax, and a clean seam to split later.