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
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 09_clean_architecture.md and 12_domain_driven_design.md 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 (../14_microservices/04_data_consistency_patterns.md) and the outbox pattern (16_transactional_outbox.md).
- 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 (../14_microservices/03_distributed_tracing.md).
- 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 19_event_driven_architecture.md and ../14_microservices/08_event_driven_microservices.md.
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. Splitting the database is the hard part — extract a service’s tables, break cross-module joins, and replace them with API calls or replicated data via events.
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
- “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.