System design deep dive
Microservices Architecture: deep dive
Microservices split an application into independently deployable services that each own one business capability and its data. They help large organizations move fast in parallel, but they replace in-process function calls with a distributed system, and interviewers want to see you weigh that cost honestly.
How a request travels through microservices, step by step
- A mobile app calls
POST /orders. The request lands at the API gateway, which validates the JWT and forwards it to the Order service with a trace ID header. - The Order service finds a healthy Inventory instance through service discovery (Kubernetes DNS, Consul, or a mesh) and calls it over gRPC with a 300 ms timeout to reserve stock.
- It then calls the Payment service. A circuit breaker wraps this call: if payment has been failing, the breaker is open and the order fails fast instead of piling up threads.
- The Order service commits the order in its own database and writes an
OrderPlacedevent to its outbox. - Shipping, Notification, and Analytics services consume that event asynchronously, each updating its own data store.
- Every hop emits spans tagged with the same trace ID, so an engineer can see in a tracing tool exactly where time was spent.
When to use microservices (and when not to)
Microservices pay off when many teams work on one product and keep blocking each other in a shared codebase and release train, when components have very different scaling needs (video encoding versus profile pages), or when parts need different technology stacks. They are usually the wrong starting point for a small team or a new product whose domain boundaries are still shifting, because drawing boundaries in the wrong place produces a distributed monolith: services that must be deployed together and call each other constantly. A well-structured modular monolith with clear internal modules can be split later along the seams that prove stable.
Monolith vs modular monolith vs microservices
| Dimension | Monolith | Modular monolith | Microservices |
|---|---|---|---|
| Deployment | One unit | One unit | Each service independently |
| Data | Shared database | Shared DB, schemas per module | Database per service |
| Calls between parts | In-process, nanoseconds | In-process, enforced interfaces | Network, milliseconds, can fail |
| Transactions | Local ACID | Local ACID | Sagas and eventual consistency |
| Operational cost | Low | Low | High: CI/CD, observability, discovery per service |
| Best fit | Small team, early product | Growing team, stable domain | Many teams, independent scaling |
Patterns you should be able to name
- Saga: a sequence of local transactions with compensating actions (refund the payment if shipping fails), coordinated by events (choreography) or a central orchestrator.
- Strangler fig: migrate from a monolith by routing one endpoint at a time to a new service until the old code can be removed.
- Backend for frontend: a thin aggregation service per client type so mobile and web each get tailored responses.
- Bulkhead: isolate resources (thread pools, connection pools) per dependency so one slow service cannot exhaust everything.
- Sidecar / service mesh: move retries, mTLS, and telemetry into a proxy next to each service rather than every codebase.
Common mistakes in interviews
- Splitting by technical layer (a database service, a validation service) instead of by business capability.
- Letting several services share one database, which couples their schemas and deployments and defeats the point.
- Long synchronous call chains: if each of 5 services is 99.9% available, the chain is about 99.5% available, and latencies add up.
- No plan for distributed tracing, centralized logging, and correlation IDs.
- Ignoring data consistency across services, or reaching for two-phase commit everywhere instead of sagas.
Interview questions
›How do you decide service boundaries?
Start from business capabilities and Domain-Driven Design bounded contexts: things that change together and share a vocabulary belong together. A good boundary means most features touch one service and the service can own its data outright. If two services are always changed and deployed together, they probably should be one.
›How do you handle a transaction that spans Order, Payment, and Inventory?
Use a saga: each service performs a local transaction and publishes an event, and on failure earlier steps are undone with compensating actions such as releasing reserved stock. The system is eventually consistent, so the UI shows states like pending until the saga completes.
›What is a circuit breaker and why is it needed?
It wraps calls to a dependency and tracks failures. After a threshold, it opens and fails calls immediately for a cool-down period, then lets a few trial calls through (half-open) to test recovery. This prevents cascading failures where threads pile up waiting on a dead service.
›How do services find each other?
Through service discovery: instances register themselves (or are registered by the orchestrator), and callers resolve a logical name to healthy addresses. In Kubernetes this is a Service with a stable DNS name; elsewhere it is Consul or Eureka, often paired with client-side load balancing.
›Sync or async communication between services?
Use synchronous REST or gRPC when the caller needs an answer to continue, and keep those chains short. Use asynchronous events through a message queue for notifications and side effects, which decouples availability and lets consumers be added without changing the producer.
