Microservices
Price a service split before drawing it: serial availability, tail amplification, and the pool math that turns one slow dependency into an outage.
A microservice architecture splits one deployable into services that each own a business capability and its data, run in their own process, and talk over the network. What it fixes is organisational — teams blocked behind one release train — and it bills for that in availability, tail latency and operations.
Price the split before you draw it
A checkout request that ran as five function calls inside one process now runs as five calls between five services. Wire time is not the problem: a round trip inside a datacenter is about 0.5 ms, so five hops add roughly 2.5 ms to a 200 ms p99 budget. Four other costs are what the split buys.
Availability multiplies. Five services on the synchronous path, each up 99.9% and each required, give 0.999^5 ≈ 0.995 — 99.5%, not 99.9%. At 99.9% you are down about 43 minutes a month; at 99.5%, 0.005 × 43,200 min ≈ 216 minutes. Holding 99.9% end to end then needs every service at about 99.98% (0.9998^5 ≈ 0.999), five redundancy budgets instead of one — or fewer services on the path, which is what an event buys: billing that consumes order.placed is off the availability chain, because orders can accept a write while billing is down. Fault isolation is a property of that arrangement, not of having services.
The tail amplifies. If each of those five calls independently exceeds its p99 one time in a hundred, the chance a request escapes all five is 0.99^5 ≈ 0.951. About 5% of requests now touch at least one p99 path: a p99 problem inside the monolith is a p95 problem across services. That, not the 2.5 ms, is the real cost of a long chain, and why tail latency worsens with every added hop.
One slow dependency drains a pool. Little's Law: concurrency = arrival rate × latency. Orders calls inventory at 200 QPS. Healthy, inventory answers in 20 ms, so 200 × 0.02 = 4 requests are in flight and a 50-connection pool idles. Inventory degrades to 800 ms — not down, just slow — and 200 × 0.8 = 160 requests need 50 connections. The pool is full, orders queues, and orders fails for reasons that have nothing to do with orders: in a monolith that slow function blocked one thread, across a network it takes down a healthy service. Timeouts and a circuit breaker are the fix, and these numbers set their thresholds.
The operations bill is per service, not per request. Each service needs a pipeline, a service-discovery entry, its own dashboards and alerts — monitoring stops being one page — and a span in a distributed trace, because a request crossing six processes cannot be read from one log file. Clients need one address, so an API gateway joins the path. The bill arrives on day one, justified split or not.
The alternative is the modular monolith: one deployable, one database, boundaries enforced in code — a schema per module, no cross-module table access, calls through a published interface. It keeps the 43-minute availability number and the in-process call, and makes a later split cheap because the seams already exist. We reject it when deploy contention is the real pain: a dozen teams on one release train, one bad migration blocking everyone's ship. That is an organisational threshold, not a traffic one — a commodity Postgres box handles roughly 5,000 simple QPS, and nothing below that says how many services to run. Independent scaling and per-service technology choice are real, but only where a component's load profile or runtime differs.
The distributed monolith
The failure mode is not too many services; it is services that cannot deploy independently. Two shapes produce it.
Shared database. Orders and billing both read and write the orders table. A column rename is now a coordinated deploy across two teams: the independent deployability that justified the split is gone, and every network cost is still paid. One service owns a schema; anyone else asks it or subscribes to its events.
Synchronous chains. Checkout calls orders calls inventory calls pricing. Every number above compounds along the chain, and retries multiply downward: three attempts at three levels is 27 calls at the bottom for one at the top, turning a slow dependency into a dead one. Retry at the edge only, with jittered backoff and a budget rather than a fixed count of attempts.
The correction for both: take everything that need not answer before the user sees a result off the request path. Orders commits its row and publishes order.placed to a broker; billing and email consume it. Delivery is at-least-once, so consumers see duplicates and every handler needs idempotency. Committing the row and then publishing is two writes with no transaction around them, so the event can vanish — the dual-write bug the outbox pattern closes. The data is now eventually consistent: the order exists before the invoice, so the UI says "processing" rather than a total that is not there yet.
In an interview
What is tested is whether you can resist the pattern whose name you know: does the split answer a constraint the interviewer gave you, and can you price it?
Phrasing that works when you propose one: "I would keep this as one deployable and split only the part with a different scaling profile — transcoding is CPU-bound and spiky, the API is not, so it becomes a worker behind a queue. The rest stays a modular monolith: 36 QPS peak does not need five deployments." Defending it: "five services on the synchronous path at 99.9% each is 99.5% end to end, so I take billing off it with an event rather than adding a fifth hop."
The mistake that loses points is opening with six boxes for a system nobody has sized yet — pattern recall rather than judgment, and the follow-ups (how do these two stay consistent, what happens when inventory takes 800 ms) land on ground you have not thought about. Close behind: services sharing one database, which an interviewer hears as a distributed monolith. Third: claiming microservices "improve scalability" without naming the component whose load profile actually differs — independent scaling is worth something only when the parts are unequal.
Check yourself
1. The p99 target is 200 ms and a request makes four sequential internal calls plus its own work. What is the per-call budget, and what does it rule out?
Reserve about 40 ms for the entry service's own work, leaving 160 ms across four calls: 40 ms per call at p99, including that callee's database time. Wire time is negligible — four datacenter round trips is about 2 ms. The 40 ms rules out a callee that itself fans out to two more services (20 ms each, before its own work), anything cross-region (India to US East is a ~200 ms round trip), and any synchronous disk-bound or batch work. Those go asynchronous, or the data is denormalised into the caller.
2. Six engineers run a monolith at 1M requests/day with a 3x peak on one Postgres box. Product asks for video transcoding. Do you split into microservices?
1M/day ≈ 12 QPS average, ~36 QPS at peak — roughly 140x under the ~5,000 QPS a single Postgres box handles, so traffic argues for nothing, and six engineers have no deploy contention either. Split exactly one thing: transcoding, because it is minutes-long, CPU-bound and spiky, and running it in the web process ties up request workers and makes every deploy a job-killer. It goes behind a queue as a consumer, not a synchronous service: the API returns
202with a job id and the client polls. Everything else stays one deployable with module boundaries.