Monoliths vs Microservices
One deployable application vs many independent services — what you actually gain and give up.
3 min read
How a backend's codebase is structured and deployed is a separate question from what language or database it uses. The two dominant patterns are the monolith and microservices, and the choice between them has real consequences for how a team builds, deploys, and debugs its system.
The monolith
A monolith is a single application — one codebase, one deployable unit — that contains all of a system's functionality: user management, orders, payments, notifications, all running as one process (or one set of identically-deployed processes behind a load balancer).
┌─────────────────────────────┐
│ Monolith │
│ users | orders | payments │
│ notifications | inventory │
└─────────────────────────────┘
│
▼
[ Database ]
Function calls between modules are just function calls — fast, and caught by the compiler/type system if you get a type wrong. Deploying, testing, and debugging happen against one artifact.
Microservices
Microservices split the same functionality into independently deployable services, each typically owning its own data, communicating over the network (HTTP, gRPC, or a message queue):
┌────────┐ ┌────────┐ ┌──────────┐ ┌───────────────┐
│ Users │ │ Orders │ │ Payments │ │ Notifications │
└───┬────┘ └───┬────┘ └────┬─────┘ └───────┬───────┘
│ │ │ │
[ DB ] [ DB ] [ DB ] [ DB ]
A function call between modules becomes a network call between services — with all the failure modes that implies: the call can time out, the other service can be down, and the response can be slow in ways a local function call never would be.
What you actually gain with microservices
- Independent deployment — the orders team can ship without coordinating with the payments team.
- Independent scaling — scale the service under heavy load (say, notifications during a promotional email blast) without scaling everything else.
- Failure isolation — a crash in the notifications service doesn't necessarily take down checkout, if the two are properly decoupled.
- Technology flexibility — different services can use different languages or databases where it genuinely helps.
What you give up
- Operational complexity — you now need service discovery, network-level retries and timeouts, and distributed tracing (see the previous lesson) just to have the same visibility a monolith gets for free.
- Data consistency gets harder — a transaction that used to be one ACID database transaction across tables now has to be coordinated across services and their separate databases, often needing patterns like sagas instead of a simple transaction.
- Slower local development — running "the whole system" locally means running many services instead of one.
- Network calls are unreliable in ways function calls aren't — every inter-service call needs its own timeout, retry, and fallback logic.
The advice that actually holds up
Start with a well-structured monolith — one codebase, but organized into clear internal modules with real boundaries between them. Split out a service only when a specific, concrete problem demands it: a module needs independent scaling, a specific team needs to deploy independently, or a piece of functionality has fundamentally different reliability requirements from the rest. Reaching for microservices on day one, before any of those pressures actually exist, mainly adds operational cost with no corresponding benefit — a mistake common enough that "premature microservices" is its own well-known anti-pattern.
Whichever architecture you choose, some work genuinely shouldn't happen inline with a request at all — which is what background jobs and queues are for, next.