#Monoliths and microservices
A monolith is one deployable unit. Orders, payments, inventory and users all live in one codebase, run in one process (or in N identical copies behind a load balancer) and usually share one database. When the order code needs stock levels it calls a function: it takes nanoseconds, it can't half-fail, and one database transaction can cover the whole operation.
A microservice architecture splits that unit into services that are deployed independently, each built around one business capability and owning its own data. The order service asks the inventory service for stock over the network. You gain independent deploys, independent scaling and smaller ownership boundaries. You pay with everything a network brings: latency, timeouts, partial failure, retries, duplicate messages and data that is only eventually consistent.
A useful mental model is a restaurant kitchen versus a food court. In the kitchen (monolith) the chefs shout across the room, share one fridge and one opening time. In the food court (microservices) every stall has its own staff, fridge and hours. Stalls can change menus and hire staff without asking each other, but a meal from three stalls now needs a runner, any stall can be closed while the others are open, and nobody can lock all three fridges at once.
My notes say "each microservice is focused on one responsibility". The responsibility to aim for is a business capability — something with its own rules, data and team, like "payments" — not one route or one table. Services that are too small ("nanoservices") have to call each other for every request, which gives you all the costs of distribution and none of the independence.
The cost of splitting is easy to underestimate, so put numbers on it. When one request needs several services to answer synchronously, their failure and slowness compound:
// A request that synchronously needs every service in the chain to succeed.
const availability = 0.999; // each service: "three nines"
for (const services of [1, 3, 5, 10]) {
const overall = availability ** services;
const failedPerMillion = Math.round((1 - overall) * 1_000_000);
console.log(`${String(services).padStart(2)} services: ${(overall * 100).toFixed(2)}% available, ~${failedPerMillion} failed requests per million`);
}
// Tail latency: if each service is slow (> its p99) 1% of the time,
// how often is at least one hop in the chain slow?
for (const hops of [1, 5, 10]) {
console.log(`${String(hops).padStart(2)} hops: ${((1 - 0.99 ** hops) * 100).toFixed(1)}% of requests hit at least one p99-slow hop`);
}
// 1 services: 99.90% available, ~1000 failed requests per million
// 3 services: 99.70% available, ~2997 failed requests per million
// 5 services: 99.50% available, ~4990 failed requests per million
// 10 services: 99.00% available, ~9955 failed requests per million
// 1 hops: 1.0% of requests hit at least one p99-slow hop
// 5 hops: 4.9% of requests hit at least one p99-slow hop
// 10 hops: 9.6% of requests hit at least one p99-slow hop- If a request succeeds only when every service in the chain succeeds, and failures are independent, the overall success probability is the product:
0.999 ** n. In a monolith the same work is a few function calls inside one process, so there is only one thing that can be down. - With one service you lose about 1,000 requests per million. With ten services in the chain,
0.999 ** 10is0.99004…, so you lose about 9,955 per million — ten times as many, even though every individual service is just as reliable as before. - The second loop applies the same arithmetic to slowness. "p99" means 1% of calls to a service are slower than that value. A request that crosses five services is unlucky on at least one hop
1 - 0.99 ** 5= 4.9% of the time; with ten hops it's 9.6%. Your end-to-end p99 is therefore much worse than any single service's p99 (this is often called tail-latency amplification). - The numbers assume failures are independent. In real systems they are often correlated (a shared database, a bad deploy, a network partition), which can make things better or much worse — but the direction is the lesson: every synchronous hop is a new way to fail.
- That's why the rest of this chapter exists: timeouts, retries, circuit breakers and asynchronous messaging are the tools that stop the product from collapsing.
What each side costs
My notes list the "cons of monolithic architecture". Here they are next to what you pay to escape them:
| Monolith | Microservices | |
|---|---|---|
| Deploys | One deploy ships everything; a bug in reports blocks a payments fix | Each service ships on its own schedule |
| Scaling | Scale the whole app to scale one hot path | Scale only the service that's hot |
| Blast radius | A memory leak or a blocked event loop in one module takes down all of them | A crash is contained to one service (if callers handle it) |
| Teams | Everyone works in one codebase and one release train | A team owns a service end to end |
| Calls | Function calls: fast, can't half-fail | Network calls: latency, timeouts, partial failure |
| Data | One database, real ACID transactions across everything | Database per service; consistency via sagas |
| Debugging | One stack trace, one log | Needs correlation IDs and distributed tracing |
| Operations | One thing to deploy and monitor | Service discovery, many pipelines, many dashboards |
The middle path is a modular monolith: one deployable, but with strict internal module boundaries (each module owns its tables and exposes a small interface). It keeps function-call speed and real transactions, and if one module later needs to scale or ship independently, the boundary is already drawn and it can be extracted.
My notes' last architecture point is statelessness. Whichever shape you choose, keep each instance free of state that only it knows (in-memory sessions, local uploads, in-process caches that must be correct). Then any instance can serve the next request, and you can add, remove or restart instances freely — see Scaling to thousands of concurrent requests and Sessions vs JWTs.
Node fits microservices well — small processes, fast start-up, cheap to run many copies, and strong at I/O-heavy glue work like gateways and backend-for-frontend services — but nothing about microservices requires Node, and nothing about Node requires microservices.
A checkout request synchronously calls four services, each with 99.95% availability. Roughly how many checkouts per million fail because of them, assuming independent failures?
Show answer
About 2,000 per million (overall availability ≈ 99.80%).
- All four must succeed, so multiply their success probabilities:
0.9995 ** 4. 0.9995 ** 2is0.99900025; squaring again gives0.9980015, about 99.80%.- The failure rate is
1 - 0.9980015=0.0019985, which is about 1,998 per million — four times the single-service figure of 500. - The fixes are architectural: take services that checkout doesn't need an immediate answer from off the synchronous path (publish an event instead), and give the remaining calls timeouts, retries and fallbacks.