Microservices are the most over-applied idea in this course. They solve a real problem, and teams routinely adopt them before they have that problem — paying every cost and receiving none of the benefit.
So this lesson is deliberately even-handed. The interview answer that impresses is not "use microservices"; it is knowing precisely what you trade.
The problem they solve
A monolith is one deployable application. It works well for a long time, then degrades in a recognisable way — and note that every symptom is organisational, not technical.
- Deployment coupling. Forty engineers merge into one codebase. Every release coordinates everyone's work, so releases get rarer, get bigger, and get riskier.
- Scaling coupling. Image processing needs CPU; the API needs memory. You can only scale the whole thing, so you over-provision everything to satisfy one part.
- Technology lock-in. The whole application is one language and one framework.
- Blast radius. A memory leak in report generation takes down checkout, because they share a process.
Microservices decouple these by making each service independently deployable, scalable and ownable.
What you pay
Every item below is a genuine, permanent cost.
Function calls become network calls. A call that was nanoseconds and could not fail becomes milliseconds and can fail, time out, or return a partial result. Every inter-service call needs timeouts, retries and a fallback.
Transactions disappear. Placing an order that spans order, payment and inventory services can no longer be a single BEGIN … COMMIT. You need sagas with compensating actions, and compensations are hard — refunding a charge is not the same as never charging.
Debugging spans machines. One request touches six services. Without distributed tracing and correlation IDs you cannot follow it, which is why those became standard practice alongside microservices rather than before them.
Operational multiplication. Twenty services means twenty deployment pipelines, twenty dashboards, twenty on-call runbooks, twenty sets of dependencies to patch.
Data consistency becomes eventual. Each service owning its own database means cross-service reads are stale by design.
Testing gets harder. Integration testing needs many services running, or a discipline of contract testing that teams often skip.
Where the boundaries go
The single most consequential decision, and the one most often got wrong.
Split by business capability, not by technical layer. A "database service" and a "business logic service" is a monolith split at the wrong axis — every feature still requires changing both, so you kept the coupling and added a network hop.
Correct boundaries look like Orders, Payments, Inventory, Notifications: each owns its data, its rules and its API, and a change to how orders work touches one service.
Two useful tests:
Does it own its data exclusively? If two services read and write the same tables, they are one service wearing two hats. Shared databases are the clearest sign of a bad split.
Can it be deployed alone? If shipping a change to A requires shipping B at the same time, the boundary is in the wrong place.
Communication
Synchronous (HTTP, gRPC) is simple and immediate, and it propagates failure: if A calls B calls C, then C's outage is A's outage, and C's latency is added to A's.
Asynchronous (events over a queue) decouples availability — the order service publishes order.placed and does not care who consumes it or whether they are up. The cost is eventual consistency and harder reasoning about flow.
The practical rule: synchronous when you need the answer to continue, asynchronous for everything else. Checkout needs a payment result now; the receipt email does not.
The patterns you must name
Distribution introduces failure modes that need explicit handling.
Circuit breaker. After repeated failures calling a service, stop trying for a while and fail fast. Without one, calls pile up waiting on a dead dependency, exhaust your thread pool, and your service dies of someone else's outage.
Timeouts everywhere. A call with no timeout is a resource leak waiting for a bad day. Set them shorter as you go deeper, so an inner call cannot outlive the outer request.
Bulkheads. Isolate resource pools per dependency so a slow service cannot consume every connection.
Retries with backoff and jitter. Immediate retries against a struggling service are how you convert a slow minute into an outage.
Saga pattern. Multi-service workflows as a sequence of local transactions, each with a compensating action if a later step fails.
Service discovery. Instances come and go; something must resolve names to current addresses.
Idempotency. Retries mean duplicates, so every operation that can be retried must be safe to repeat.
- The gateway forwards to the orders service.
- Orders calls payments synchronously, then publishes an event to a queue.
- Notifications and inventory consume the queue independently, off the request path.
Describe this diagram
Components
- client (Client)
- gateway (API gateway)
- orders (Service)
- payments (Service)
- queue (SQS queue)
- notifications (Service)
Connections
- client sends to gateway
- gateway sends to orders
- orders sends to payments
- orders sends to queue
- queue sends to notifications
How to get there
If you decide the trade is worth it, do not rewrite.
Start with a monolith. You do not understand your domain well enough to draw boundaries on day one, and boundaries are expensive to move once they are network calls.
Use the strangler pattern. Route specific functionality out to a new service behind the existing interface, one capability at a time. The monolith shrinks gradually; nothing needs a big-bang cutover.
Extract by pain. Split out the part that is genuinely blocked on deployment coupling or needs different scaling. Do not split by tidiness.
Keep a modular monolith as the default. Clear module boundaries, separate schemas per module, no cross-module table access — all within one deployable. You get most of the organisational clarity with none of the distributed-systems cost, and if you later need to extract a module, well-defined boundaries make it straightforward.
Check your understanding
Common mistakes
Adopting them for scale you do not have. One well-built monolith serves enormous traffic. Microservices are for organisational scale first.
Splitting by technical layer. Boundaries belong on business capabilities.
Sharing a database. The clearest indicator of a bad split, and it removes any independence you thought you had.
Synchronous chains several deep. Multiplied latency and multiplied failure probability. Use events.
No distributed tracing. You will be unable to answer "why was that request slow?"
Services too small. A service per entity produces enormous chattiness and coordination overhead. A service should own a capability, not a table.
Practice
A 30-engineer team runs a monolith. Deploys take an hour and one team's bug blocks everyone's release. They want microservices. What would you do first, and what would you extract first if you did split?
Write your own answer first, on paper or in a note. Then compare.
Show a hint
Ask whether the pain is deployment, scaling, or ownership — the remedies differ.
Show a strong answer
Diagnose before splitting, because the described pain has cheaper cures.
- An hour-long deploy is usually a build and test problem, not an architecture problem. Parallelising tests, caching builds and shipping smaller changes often reduces it dramatically, and none of that costs a distributed system.
- One bug blocking everyone is a release-process problem. Feature flags and the ability to roll back one change independently address it directly.
At 30 engineers, the honest answer is that a well-structured monolith is usually still the right choice. Microservices solve organisational scaling — many teams shipping independently — and they charge for it in network calls, partial failures, distributed debugging and operational load. Thirty people can typically coordinate.
If you do split, extract in this order:
- Something with a clear boundary and different scaling needs — image processing, notifications, report generation. These are naturally separate, have few dependencies, and prove the operational tooling on something low risk.
- Something owned end to end by one team, so the boundary matches a team boundary and independent deploy actually materialises.
- Never extract the shared data model first. Whatever every feature touches is the hardest thing to separate and the worst first attempt.
What must exist before the second service ships: centralised logging with request correlation, distributed tracing, and a deployment pipeline that does not require a human per service. Without these, debugging goes from hard to impossible, and the split makes the team slower rather than faster.
Your order service needs customer addresses owned by the customer service. Compare calling it synchronously versus subscribing to change events, and pick one.
Write your own answer first, on paper or in a note. Then compare.
Show a hint
Ask what happens to orders when the customer service is down.
Show a strong answer
Synchronous call.
- For: always current, trivial to implement, no duplicated storage, and no stale data to reason about.
- Against: the order service now cannot place an order when the customer service is down. You have made a core revenue path depend on the availability of a supporting service, and its latency is added to every order. Availability multiplies: two services at 99.9% give 99.8% on this path.
Subscribe to change events and keep a local copy.
- For: orders can be placed when the customer service is down, latency is a local read, and the order service controls its own availability.
- Against: the copy can be stale, you now store data you do not own, and you must handle the initial backfill plus missed events.
I would choose events, for a specific reason: an order should capture the address as it was at the time of the order anyway. A shipping address is not a live lookup — it is a fact recorded at purchase time, and if the customer changes their address next month the old order must not silently change destination. So the local copy is not merely a cache; it is the correct domain model.
What would change my mind: if the requirement were "validate this address is currently deliverable", that is a live question about the present, and a synchronous call to the owner is right — with a timeout, a circuit breaker, and a decision made in advance about whether to accept the order when the check cannot be performed.
Key takeaways
- The real driver is team autonomy, not performance — the benefit scales with headcount, and is usually negative for small teams.
- Costs are permanent: network calls that fail, no cross-service transactions, distributed debugging, operational multiplication, eventual consistency.
- Split by business capability, and test the boundary with: does it own its data exclusively, and can it be deployed alone?
- A shared database means you have one service, not several.
- The distributed monolith is worse than a monolith — every cost of distribution, none of the independence.
- Synchronous only when you need the answer to continue; events for everything else.
- Name the resilience patterns: circuit breakers, timeouts, bulkheads, backoff with jitter, sagas, idempotency.
- Start with a modular monolith and extract with the strangler pattern, driven by real pain.
Further reading
- Sam Newman, Building Microservices, second edition (O'Reilly, 2021).
- Martin Fowler and James Lewis, "Microservices" (2014) — the article that named the pattern, including the prerequisites.
- Martin Fowler, "MonolithFirst" (2015) — the argument for not starting with services.
- Eric Evans, Domain-Driven Design (Addison-Wesley, 2003) — bounded contexts, which is where boundaries should come from.
- Melvin Conway, "How Do Committees Invent?" (Datamation, 1968) — why architecture follows communication structure.