Microservices

Lesson 2.3 · 8 min read · by Vinay C

We'll cover the following

Monolith vs microservices: good or bad?

Neither. It depends on context. Microservices trade code complexity for distributed-systems complexity.

What you getWhat you pay
MicroservicesIndependent scaling, independent deploys, freedom to pick tech per service, team autonomy (no waiting on other teams)Network calls, partial failures, data consistency across services, many deployments to run, much harder debugging
MonolithOne deploy, simple debugging, in-process calls, easy transactionsEverything scales and deploys together, teams step on each other as it grows
  • Good when: many teams that must deploy independently, parts of the system with very different scale, a clear domain.
  • Bad when: a small team or an early product. The overhead outweighs the benefit.
  • Not automatically "no single point of failure". There are more places to fail, and one slow service can drag down the others (cascading failure). Isolation has to be designed.
  • Start with a modular monolith and split only under team or scale pressure.

Sync vs async between services

The real question is "does the caller need the answer right now to continue?", not whether the call is consistency-related.

  • Sync (REST / gRPC): the caller waits. Use it when the response drives the next step: "is this card valid?", "what is the price?".
  • Async (queue / events): the caller hands off and moves on. Use it for work that can finish later: sending the email, updating analytics, the next step of an order saga.
  • A bank transfer can still use events internally, with a saga for consistency.

Database per service

Can two services share one database? It can work, but it is the anti-pattern, because it defeats the point of microservices:

  • Hidden coupling: change a column and you may break another team service.
  • No independent deploys or scaling, and one heavy query slows everyone.
  • Unclear ownership: who validates the data, and who may write it?
  • The cost of doing it right: no cross-service joins, so you use APIs, events and sagas.
  • Sharing is tolerable only as a temporary step during a migration.

The rule: each service owns its data. Other services get it through the owner API or events, never through its tables.

When services need overlapping data

First question: who is the source of truth? Then pick one:

OptionHow it worksTrade-off
One owner + APIThe Customer service owns customers. Orders calls GET /customers/42 when it needs a name.Simple, but adds a network call and a runtime dependency
Event-fed local copyThe owner publishes CustomerUpdated. Other services keep their own read-only copy of just the fields they need.Fast, no runtime dependency, but eventually consistent
Snapshot at time of useOrders stores customer_id plus the customer name and address as they were at order time.Often the correct behavior anyway: an old invoice must not change when the customer moves
Revisit the boundaryIf two services constantly need the same tables and change together, they may really be one service.Overlap is a sign the split is wrong
Static reference dataCountry list, currencies: ship as a shared library or config, or a small read-only service.Needs a rollout when it changes
Cross-service reportingReplicate everything into a data warehouse or read model (CQRS) instead of joining live tables.Reports lag slightly

Failure isolation: when B is slow or down

A fallback (error, default, notify someone) is only one tool. The full toolkit, used as layers:

  • Timeouts: never wait forever. Without them, one slow B ties up all of A threads and A dies too.
  • Retries with exponential backoff and jitter, only for idempotent calls.
  • Circuit breaker: after N failures A stops calling B for a while and fails fast. States: closed (normal), open (calls blocked), half-open (a few trial calls to see if B recovered).
  • Bulkheads: a separate thread pool per dependency, so B trouble cannot use up everything A has.
  • Fallbacks and graceful degradation: cached data, defaults, or hiding a feature.
  • Load shedding and rate limiting to protect B while it recovers.

Worked example: B goes from 50 ms to 30 s

  • A has 200 threads and no timeout. Each call to B now holds a thread for 30 s, so A can handle only about 7 calls per second (200 / 30).
  • At 500 requests per second, all 200 threads are stuck within a fraction of a second.
  • A then cannot serve anything, even requests that never touch B. It looks down, and A callers start failing: a cascading failure.
  • Order to add the fixes: (1) timeout (cheapest, frees threads), (2) circuit breaker (it depends on timeouts to even detect that calls are failing), (3) bulkhead, (4) fallback.
Check yourselfB starts responding in 30 seconds instead of 50 ms. A has 200 worker threads and no timeout. What happens to A in the next minute, and what do you add first?

All 200 threads block on B within a fraction of a second (capacity drops to about 7 calls/s), so A stops serving everything and the failure cascades upstream. Add a timeout first (for example 500 ms) so threads are freed, then a circuit breaker (which needs timeouts to detect failures), then a bulkhead and a fallback.

Summary: say it in the interview

  • "I would start modular and split when team size or scale pressure demands it. Microservices are a trade, not an upgrade."
  • "Sync when I need the answer, async when I can decouple in time."
  • "One service owns each piece of data. Others read through its API or keep an event-fed local copy. If two services keep needing the same tables, I would revisit the boundary."
  • "Timeouts everywhere, retries with backoff on idempotent calls, a circuit breaker per dependency, bulkheads, and a defined fallback. They work as layers."