Microservices Interview Questions — 35 Deep Answers
Microservices interviews at senior and architect level are not about buzzwords. The interviewer wants to see whether you have actually operated distributed systems under production load — whether you have debugged a cascade failure at 2 AM, designed a saga that had to compensate for a half-committed database, or decided when NOT to split a service further.
This guide covers 35 questions across every major microservices topic: monolith vs microservices trade-offs, service decomposition, synchronous and asynchronous communication, sagas, event sourcing, CQRS, circuit breakers, API gateways, service discovery, distributed tracing, database per service, and strangler-fig migration. Every answer is concrete, includes real examples, and notes what interviewers are actually testing.
Part 1 — Monolith vs Microservices Trade-offs
1. What problem does the microservices architectural style solve, and when does it make things worse?
What the interviewer is testing: Whether you see microservices as a tool with a cost/benefit, not as a universal upgrade.
Answer:
A monolith becomes painful when two teams cannot deploy independently, when a memory leak in the billing module takes down checkout, or when you need to scale one hot code path (e.g., image processing) without scaling the whole application.
Microservices solve:
- Independent deployability — teams own services end-to-end and ship without coordinating releases.
- Fault isolation — a crashed recommendation service does not crash order placement.
- Technology heterogeneity — the ML team can run Python while the payments team runs Go.
- Targeted scaling — scale only the services under load.
Microservices make things worse when:
- The team is fewer than ~10 engineers. The operational overhead (N service CI pipelines, distributed tracing, network latency debugging) consumes all engineering time.
- The domain model is not well understood yet. Splitting prematurely creates tight coupling across service boundaries, which is harder to refactor than coupling inside a monolith.
- The organization lacks the DevOps maturity to run containers, health checks, secrets management, and on-call rotations per service.
Concrete example: Amazon started as a monolith. Netflix migrated from a single Oracle database and monolithic application to microservices over five years, not overnight, because they understood the cost.
What to say in the interview: "Microservices shift complexity from code to infrastructure and organizational coordination. The question is always: does the benefit of independent deployability justify that cost right now?"
2. How do you decide the right size for a microservice?
What the interviewer is testing: Whether you use Domain-Driven Design concepts and practical heuristics, not just "small."
Answer:
"Micro" is a terrible guide. The right size is determined by:
Bounded context (DDD): A service should own one bounded context — a cohesive set of domain concepts with one ubiquitous language. The Order service understands Order, LineItem, Discount. It does not leak into the Product catalog's SKU or the Warehouse's PickingList.
Single reason to change: If changing the tax calculation logic forces you to redeploy user authentication, the boundary is wrong.
Team ownership: A service should be small enough that one team can hold the entire mental model in their heads, and no larger.
Practical heuristics:
- A service that fits in ~1,000–5,000 lines of core domain code (not counting generated code or tests) is usually well-scoped.
- If you find yourself doing network calls between two services 95% of the time together, they might be the same bounded context.
- If the service has no state and only transforms data, ask whether it is a library, not a service.
Common mistake to avoid: Splitting by technical layer (a "database service," a "validation service") rather than by domain. This creates chatty inter-service communication and no real independence.
3. Describe the organizational implications of microservices (Conway's Law).
Answer:
Conway's Law states that organizations design systems that mirror their communication structures. This is not metaphor — it is an empirical constraint.
If your Payments team and your Risk team must coordinate every time they deploy, they will eventually want a shared codebase, and you will end up with an accidental monolith. If they own fully separate services with a stable API contract between them, they operate independently.
The Inverse Conway Maneuver: deliberately structure your teams first — small, cross-functional teams owning end-to-end slices of the product — and let the service boundaries follow the team structure.
Practical implication: before drawing a service boundary on a whiteboard, draw the org chart. If two "separate services" are owned by the same five engineers, you have added operational complexity with zero organizational gain.
4. What is the distributed monolith anti-pattern and how do you avoid it?
Answer:
A distributed monolith looks like microservices (many deployable units, network calls between them) but behaves like a monolith (services must be deployed together, share a database, or fail together).
Signs you have a distributed monolith:
- Services share a single database schema and query each other's tables directly.
- Deploying Service A requires deploying Service B first.
- A single user request fans out to 15 synchronous service calls, all of which must succeed.
- There is one team that "owns" the deployment pipeline for all services.
How to avoid it:
- 1Each service owns its data — no shared database tables, no cross-service foreign keys enforced by the database.
- 2API contracts are versioned — consumers are not broken by a producer's internal refactoring.
- 3Deployments are truly independent — use feature flags and backwards-compatible migrations to allow services to diverge in deployment timeline.
- 4Avoid synchronous call chains longer than 2–3 hops — if request A → B → C → D is synchronous, that is a distributed monolith in behavior (one failure breaks all four).
Part 2 — Service Decomposition
5. Walk me through decomposing a traditional e-commerce monolith into microservices.
Answer:
Start by identifying bounded contexts using DDD event storming. Run a workshop where domain experts and engineers write domain events on sticky notes in chronological order: ProductViewed, AddedToCart, OrderPlaced, PaymentProcessed, InventoryReserved, ShipmentDispatched, OrderDelivered.
Cluster the events by the aggregates and domain concepts they belong to:
| Bounded Context | Core Aggregates | Candidate Service |
|---|---|---|
| Product Catalog | Product, Category, Price | Catalog Service |
| Shopping Cart | Cart, CartItem | Cart Service |
| Order Management | Order, LineItem | Order Service |
| Payment | Payment, Refund | Payment Service |
| Inventory | StockLevel, Reservation | Inventory Service |
| Shipping | Shipment, Carrier, TrackingEvent | Shipping Service |
| Identity | User, Address, Session | Identity Service |
Then apply the strangler-fig pattern to migrate incrementally (see Question 35).
What interviewers look for: Candidates who mention DDD event storming score much higher than those who just say "split by entity."
6. How do you handle shared domain concepts across services?
Answer:
This is one of the hardest problems in microservices. User or Product concepts appear in almost every service, but you cannot let every service call the User service for every request.
Strategies:
Data replication via events: The User service publishes UserCreated / UserUpdated events. The Order service subscribes and stores only the fields it needs (userId, email for receipts). This is denormalization by design — the Order service has a local copy of user data it cares about.
Shared kernel: If two services are in the same team and domain, they can share a small library of value objects (e.g., Money, Currency, Address). Keep this kernel tiny and change it rarely.
Context mapping with translation: If the Product service calls its aggregate a Product and the Shipping service calls the same thing a Package, that is fine — each service has its own model. Use an anti-corruption layer to translate between them at the boundary.
Never do: Cross-service joins at the database level, or synchronous real-time calls to get "fresh" data on every request if that data changes rarely.
7. What is the difference between functional and non-functional decomposition?
Answer:
Functional decomposition splits services by business capability: Order Service, Payment Service, Notification Service. This aligns with DDD bounded contexts.
Non-functional decomposition splits by technical or operational concerns: a CDN-facing read-only service vs. a write-heavy transactional service for the same domain, or a high-security PCI-scoped payment service isolated from general traffic for compliance reasons.
In practice, you combine both. A Pricing Service might be split out not because it is a separate business domain, but because the pricing engine needs different scaling characteristics (CPU-heavy, read-mostly, can be cached aggressively) from the Order Service.
Part 3 — Synchronous Communication (REST / gRPC)
8. When do you choose REST over gRPC for inter-service communication?
Answer:
| Criterion | REST (HTTP/JSON) | gRPC (HTTP/2 + Protobuf) |
|---|---|---|
| Human readability | High (curl-friendly) | Low (binary protocol) |
| Payload size | Larger (text JSON) | Smaller (binary Protobuf, typically 3–10× smaller) |
| Type safety | Optional (OpenAPI) | Enforced (proto schema) |
| Streaming | Limited (SSE, WebSocket workaround) | Native bidirectional streaming |
| Cross-language codegen | Good (OpenAPI generators) | Excellent (proto generates client/server stubs) |
| Browser support | Native | Requires grpc-web proxy |
| Latency | Higher | Lower (multiplexed HTTP/2, binary serialization) |
Choose REST when:
- The API is public-facing or consumed by browsers.
- The team values debuggability over performance.
- Partners or third parties consume the API (ecosystem tooling is richer for REST).
Choose gRPC when:
- Internal service-to-service calls where latency matters.
- You need bidirectional streaming (e.g., real-time telemetry, chat).
- Strict API contracts across polyglot services (proto files as the source of truth).
Example: Google uses gRPC internally for all inter-service calls; Stripe uses REST for its public API. Netflix uses both.
9. How do you implement service-to-service authentication?
Answer:
In a microservices cluster, never assume internal traffic is trusted. Zero-trust networking requires every service to authenticate and authorize every request.
mTLS (mutual TLS): Both client and server present certificates. The service mesh (Istio, Linkerd) handles certificate rotation automatically. No application code changes needed.
# Istio PeerAuthentication — require mTLS for all traffic in namespace
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
name: default
namespace: production
spec:
mtls:
mode: STRICTJWT service tokens: Each service has a service account. When calling another service, it presents a short-lived JWT signed by a central auth server (e.g., Vault, AWS IAM). The receiving service validates the JWT's sub (service identity) and scope.
// Middleware that validates service-to-service JWT
func ServiceAuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("X-Service-Token")
claims, err := validateServiceJWT(token)
if err != nil || !isAllowedService(claims.Subject) {
http.Error(w, "Unauthorized", 401)
return
}
next.ServeHTTP(w, r)
})
}SPIFFE/SPIRE: The open standard for workload identity in cloud-native environments. Each workload gets a SPIFFE ID (a URI like spiffe://example.com/ns/payments/sa/payment-service), which acts as its cryptographic identity.
10. What is backpressure and how do you implement it in synchronous service calls?
Answer:
Backpressure is a mechanism by which a downstream service signals to its callers that it is overloaded, so callers slow down rather than making the overload worse.
In a synchronous HTTP call chain, backpressure manifests as:
- 1HTTP 429 Too Many Requests with a
Retry-Afterheader. - 2HTTP 503 Service Unavailable when the service has shed load.
- 3Timeout propagation — each hop in a call chain should propagate the remaining deadline (
grpc-timeoutheader in gRPC, or aX-Request-Deadlinecustom header in REST).
// gRPC client with deadline propagation
ctx, cancel := context.WithTimeout(ctx, 500*time.Millisecond)
defer cancel()
resp, err := inventoryClient.CheckStock(ctx, &pb.CheckStockRequest{
ProductId: productId,
Quantity: qty,
})
if status.Code(err) == codes.DeadlineExceeded {
// Return a degraded response rather than waiting further
return fallbackResponse()
}Reactive Streams (in JVM ecosystems with Reactor or RxJava) implement backpressure natively: the subscriber signals demand to the publisher and the publisher does not emit faster than the subscriber can process.
Part 4 — Asynchronous Communication (Kafka / RabbitMQ)
11. When do you choose asynchronous messaging over synchronous REST calls?
Answer:
Use asynchronous messaging when:
- 1The caller does not need an immediate response. Sending a confirmation email after order placement does not require the order service to wait for the email service to finish.
- 2You need to decouple service availability. If the Notification service is down, the Order service should not fail. A message broker buffers the event until Notification recovers.
- 3You need fan-out to multiple consumers.
OrderPlacedneeds to be consumed by Inventory, Billing, Analytics, and Notification. Publishing one event is cleaner than four synchronous calls. - 4You need replay/audit trails. Kafka retains events for configurable periods, enabling consumers to replay history.
Use synchronous calls when:
- The response is needed to continue the current operation (e.g., check inventory before confirming an order).
- The operation is a query, not a command.
- You need strong consistency guarantees within a transaction boundary.
12. Explain Kafka's architecture and why it is durable.
Answer:
Kafka is a distributed commit log. Messages are written to partitions of a topic. Each partition is an append-only, ordered sequence of records stored on disk.
Durability comes from:
- Replication factor: Each partition is replicated across N brokers (typically 3). A write is acknowledged only after it is written to the leader and
min.insync.replicasfollowers (typically 2). - Durable storage: Unlike RabbitMQ's in-memory queue model, Kafka writes to disk by default.
- Offset management: Each consumer maintains its own offset (position in the log). If a consumer crashes, it resumes from the last committed offset rather than losing messages.
# Producer config for strong durability
acks=all
retries=Integer.MAX_VALUE
max.in.flight.requests.per.connection=1
enable.idempotence=trueKafka vs RabbitMQ:
- RabbitMQ is a traditional message broker: message is delivered to a consumer and deleted. Better for task queues where you want work distributed across workers.
- Kafka is an event log: the record persists. Better for event sourcing, audit trails, multiple independent consumers on the same stream.
13. How do you guarantee exactly-once delivery in a distributed messaging system?
Answer:
Exactly-once semantics requires coordination at both the producer and consumer sides. In practice, most systems implement at-least-once delivery with idempotent consumers, which achieves the same outcome.
Kafka exactly-once (since 0.11):
// Transactional producer
producer.initTransactions();
try {
producer.beginTransaction();
producer.send(new ProducerRecord<>("orders", key, value));
producer.send(new ProducerRecord<>("inventory-reservations", key, value));
producer.commitTransaction();
} catch (ProducerFencedException e) {
producer.close();
} catch (KafkaException e) {
producer.abortTransaction();
}Kafka's transactional API ensures that a batch of writes across multiple topics either all commit or all abort, and that the same batch is never committed twice (via epoch-based fencing of zombie producers).
Idempotent consumer pattern (more common in practice):
-- Deduplicate by event ID before processing
CREATE TABLE processed_events (
event_id UUID PRIMARY KEY,
processed_at TIMESTAMP
);
BEGIN;
INSERT INTO processed_events (event_id, processed_at)
VALUES ($1, NOW())
ON CONFLICT (event_id) DO NOTHING;
-- If inserted (not a duplicate), apply business logic
-- If conflict, skip silently
COMMIT;Every event carries a unique eventId. The consumer checks its deduplication table before processing. This makes the consumer idempotent regardless of how many times the message is delivered.
14. What is the outbox pattern and why is it essential for reliable event publishing?
Answer:
The outbox pattern solves the dual-write problem: you cannot atomically update your database AND publish a message to Kafka/RabbitMQ in one step. If you write to the database first and then the application crashes before publishing, the event is lost. If you publish first and then the database write fails, you have a spurious event.
Solution:
-- Application writes to both tables in ONE database transaction
BEGIN;
INSERT INTO orders (id, user_id, total, status)
VALUES ($1, $2, $3, 'PENDING');
INSERT INTO outbox (id, aggregate_type, aggregate_id, event_type, payload, published)
VALUES (gen_random_uuid(), 'Order', $1, 'OrderPlaced', $4::jsonb, false);
COMMIT;A separate outbox relay process (or Debezium CDC connector) polls the outbox table and publishes unpublished events to the message broker, then marks them as published.
-- Outbox relay
SELECT * FROM outbox WHERE published = false ORDER BY created_at LIMIT 100;
-- publish each to Kafka
UPDATE outbox SET published = true WHERE id = ANY($1);With Debezium (CDC-based outbox): Debezium streams the database's WAL (write-ahead log) directly to Kafka. No polling needed, sub-second latency, and no risk of missing events.
Part 5 — Saga Pattern
15. What is the saga pattern and why do you need it in microservices?
Answer:
In a monolith, you use a single ACID database transaction to maintain consistency across multiple operations. In microservices, each service has its own database — you cannot use a single database transaction across service boundaries.
The saga pattern is a sequence of local transactions where each step publishes an event or sends a message that triggers the next step. If any step fails, the saga executes compensating transactions to undo the preceding steps.
Example: Hotel + Flight + Car Booking
Forward saga:
- 1Book hotel →
HotelBookedevent - 2Book flight →
FlightBookedevent - 3Charge credit card →
PaymentProcessedevent → saga complete
Compensating (if step 3 fails):
- 1Cancel flight →
FlightCancelled - 2Cancel hotel →
HotelCancelled
Compensating transactions must be idempotent and retryable — they will run at least once, possibly more.
16. Choreography vs orchestration sagas — when do you use each?
Answer:
Choreography: Services react to events without a central coordinator. The Order service publishes OrderCreated. Inventory subscribes and reserves stock, then publishes StockReserved. Payment subscribes to that and charges the card.
OrderService ──▶ [OrderCreated] ──▶ InventoryService
│
[StockReserved]
│
▼
PaymentService
│
[PaymentCharged]Pros: No single point of failure, services are loosely coupled.
Cons: The saga flow is implicit and distributed — hard to understand at a glance, hard to debug, hard to handle complex branching logic.
Orchestration: A central saga orchestrator (a dedicated service or workflow engine) sends commands to services and tracks state.
class BookingOrchestrator:
def execute(self, booking_id):
# Step 1
hotel_result = hotel_service.book(booking_id)
if hotel_result.failed:
return # nothing to compensate
# Step 2
flight_result = flight_service.book(booking_id)
if flight_result.failed:
hotel_service.cancel(booking_id) # compensate
return
# Step 3
payment_result = payment_service.charge(booking_id)
if payment_result.failed:
flight_service.cancel(booking_id)
hotel_service.cancel(booking_id)
returnPros: Flow is centralized, easy to monitor, complex branching is manageable.
Cons: The orchestrator is a central dependency; needs its own high availability.
Rule of thumb: Use choreography for simple linear flows with 2–3 steps. Use orchestration for complex workflows, workflows with branching, or workflows that need explicit state visibility.
Popular orchestration engines: Temporal, AWS Step Functions, Conductor (Netflix), Camunda.
17. How do you handle a saga that is stuck in a partially committed state?
Answer:
A saga in a partially committed state (some steps done, others not yet) is normal — it is always in a transitional state. The problem is when it gets stuck (a service is down, a compensating transaction fails, or the orchestrator itself crashes).
Solutions:
- 1Persistent saga state: Store the saga's current step and all relevant data in a durable store (database, not in-memory). When the orchestrator restarts, it resumes from where it left off.
CREATE TABLE saga_state (
saga_id UUID PRIMARY KEY,
saga_type TEXT,
current_step TEXT,
status TEXT, -- RUNNING, COMPLETED, COMPENSATING, FAILED
context JSONB,
created_at TIMESTAMP,
updated_at TIMESTAMP
);- 2Idempotent compensation: Design compensating transactions to be safe to retry. Cancelling a booking twice should be a no-op, not an error.
- 3Dead-letter queue (DLQ) + manual intervention: If a compensating transaction fails after N retries, move the saga to a DLQ and alert an operator. Some partial states require human resolution (e.g., a charge completed but fulfillment failed — issue a refund manually).
- 4Timeout-based cleanup: Use a background job that finds sagas in RUNNING state older than X minutes and marks them for compensation.
Part 6 — Event Sourcing and CQRS
18. What is event sourcing and what problem does it solve?
Answer:
In traditional CRUD, you store the current state: order.status = 'SHIPPED'. If you want to know why the order is in that state, you look at logs — if they exist.
In event sourcing, you store the sequence of events that led to the current state:
OrderPlaced { orderId, items, total, timestamp }
PaymentConfirmed { orderId, paymentId, timestamp }
InventoryReserved { orderId, warehouseId, timestamp }
OrderShipped { orderId, trackingNumber, timestamp }Current state is derived by replaying these events.
Problems it solves:
- Complete audit trail — you know not just what happened, but when, why, and in what sequence.
- Temporal queries — reconstruct the state of an order as it was at any point in time.
- Event replay for new features — build a new read model by replaying historical events.
- Debugging — reproduce production bugs by replaying the exact event sequence.
Problems it introduces:
- Query complexity — you cannot
SELECT * FROM orders WHERE status = 'SHIPPED'directly. You need projections. - Schema evolution — events are immutable; changing an event's shape requires versioning.
- Storage growth — the event log grows forever (use snapshotting to compress old history).
19. Explain CQRS and how it complements event sourcing.
Answer:
CQRS (Command Query Responsibility Segregation) separates the write model (commands) from the read model (queries). They can use different data stores, different schemas, and scale independently.
WRITE SIDE READ SIDE
────────── ─────────
Client ──▶ Command ──▶ Command Handler Query ──▶ Query Handler
│ │
Event Store Read Model DB
(append (PostgreSQL, Redis,
only) Elasticsearch)
│
Event published ──────────────▶ Projection
(updates read DB)Example: An e-commerce order service.
Write side: PlaceOrderCommand → handler validates, creates events [OrderPlaced, ItemsReserved], appends to event store.
Read side: A projection consumes OrderPlaced events and writes denormalized rows to a orders_view table optimized for the exact queries the UI needs (SELECT order_id, status, total, customer_name FROM orders_view WHERE customer_id = ?).
When to use CQRS:
- High read-to-write ratio where read and write models need different optimizations.
- Complex domain where the write model benefits from event sourcing's audit trail.
- You need multiple specialized read models for different consumers (mobile app, analytics, admin UI).
When NOT to use it: Simple CRUD applications. CQRS adds significant complexity. Do not use it unless you need it.
20. How do you handle eventual consistency in a CQRS read model?
Answer:
After a command is processed and an event is published, the read model is updated asynchronously. There is a lag — typically milliseconds to seconds. This is eventual consistency.
Strategies for the UI:
- 1Optimistic UI updates: The client immediately reflects the command result locally (e.g., show the new order in the list) before the read model is updated. If the command fails, revert.
- 2Read-your-writes via version tracking: Include a
versionoreventIdin the command response. The client polls the read model until it sees that version, confirming the projection has caught up.
// After placing order
const { orderId, version } = await placeOrder(orderData);
// Poll until read model reflects the new version
await pollUntil(
() => fetchOrder(orderId),
(order) => order.version >= version,
{ timeout: 5000, interval: 200 }
);- 3Read from the write side: For immediate post-write reads (e.g., show confirmation details), read directly from the event store or write database, bypassing the eventual-consistent read model.
Part 7 — Circuit Breaker
21. Explain the circuit breaker pattern and its state transitions.
Answer:
The circuit breaker prevents a service from repeatedly calling a failing downstream service, which would exhaust connection pools, waste request budget, and slow the caller down while waiting for timeouts.
States:
(failure threshold exceeded)
CLOSED ─────────────────────────────▶ OPEN
▲ │
│ (probe succeeds) │ (wait period expires)
│ ▼
└──────────────────────────── HALF-OPEN- CLOSED (normal): Requests pass through. Failures are counted. When failures exceed the threshold within a time window, trip to OPEN.
- OPEN (tripped): All requests fail immediately without calling the downstream service. Returns a fallback or error instantly.
- HALF-OPEN (probe): After the wait period, allow a limited number of test requests through. If they succeed, close the circuit. If they fail, reopen.
Implementation with Resilience4j (Java):
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
.failureRateThreshold(50) // trip at 50% failure rate
.waitDurationInOpenState(Duration.ofSeconds(30))
.permittedNumberOfCallsInHalfOpenState(3)
.slidingWindowSize(10)
.build();
CircuitBreaker cb = CircuitBreakerRegistry.of(config)
.circuitBreaker("inventory-service");
Supplier<InventoryResponse> decoratedCall = CircuitBreaker
.decorateSupplier(cb, () -> inventoryClient.checkStock(productId));
Try<InventoryResponse> result = Try.ofSupplier(decoratedCall)
.recover(CallNotPermittedException.class, ex -> fallbackResponse());What interviewers look for: Knowing about the half-open state (many candidates forget it), understanding that the fallback response must be a deliberate design choice (not just "return null"), and awareness of bulkhead pattern as a complement.
22. What is the bulkhead pattern and how does it differ from the circuit breaker?
Answer:
The bulkhead pattern isolates resources so that a failure in one area does not exhaust resources for another. Named after watertight compartments in a ship's hull.
Implementation: Use separate thread pools or connection pools for different downstream services.
// Without bulkhead: all services share one thread pool
// A slow InventoryService hogs all 200 threads, starving PaymentService calls
// With bulkhead: separate pools
ThreadPoolBulkheadConfig inventoryBulkhead = ThreadPoolBulkheadConfig.custom()
.maxThreadPoolSize(10)
.coreThreadPoolSize(5)
.queueCapacity(20)
.build();
ThreadPoolBulkheadConfig paymentBulkhead = ThreadPoolBulkheadConfig.custom()
.maxThreadPoolSize(10)
.coreThreadPoolSize(5)
.queueCapacity(10)
.build();Circuit breaker answers: "Is this service healthy enough to call?" It fails fast when the service is down.
Bulkhead answers: "How many resources am I willing to dedicate to calling this service?" It limits blast radius.
Use both together: the circuit breaker avoids unnecessary calls to unhealthy services; the bulkhead limits the damage when a service is slow but not down.
Part 8 — API Gateway
23. What does an API gateway do and what should it NOT do?
Answer:
An API gateway is the single entry point for external clients. It handles cross-cutting concerns so individual services do not have to:
Should do:
- Routing and load balancing — route
/api/ordersto the Order service. - Authentication — validate JWTs, extract user identity, pass it downstream as a verified header.
- Rate limiting — enforce per-client or per-endpoint rate limits.
- SSL termination — handle TLS so internal services communicate over plain HTTP within the cluster.
- Request/response transformation — version adapters, field filtering.
- Observability — centralized access logging, latency metrics per endpoint.
Should NOT do:
- Business logic — if the gateway checks whether a user has enough credit, you have put domain logic in infrastructure.
- Aggregation of every response — the Backend for Frontend (BFF) pattern is better for complex aggregations tailored to specific clients.
- Service discovery — the gateway queries the service registry, but does not own it.
Popular implementations: Kong, AWS API Gateway, Nginx + Lua, Envoy, Traefik, Google Cloud Apigee.
24. What is the Backend for Frontend (BFF) pattern?
Answer:
A single API gateway serving both a mobile app and a desktop web app creates a tension: the mobile app wants lightweight responses (low bandwidth), the desktop wants rich data (more complex queries). Adding mobile-specific compression or query parameters to the shared gateway makes it grow into a mess.
BFF solution: Create one backend per client type, each tailored to its client's needs.
Mobile App ──▶ Mobile BFF ──┐
├──▶ Order Service
Web App ──▶ Web BFF ──┤──▶ User Service
└──▶ Product Service
Third Party ──▶ Public APIEach BFF is owned by the team that builds its corresponding frontend. They can iterate on the API shape without coordinating with other teams.
When to use: When you have meaningfully different clients with different data needs. When one team owns the frontend+backend slice.
When NOT to use: When you have only one client type. A BFF per microservice is an anti-pattern.
Part 9 — Service Discovery
25. Explain client-side vs server-side service discovery.
Answer:
In a microservices cluster, service instances start and stop dynamically (scale up, health check failures, rolling deploys). Service discovery is the mechanism by which services find each other.
Client-side discovery:
The client queries the service registry (e.g., Consul, Eureka) to get the list of healthy instances, then load-balances the request itself.
// Client-side with Ribbon (Netflix)
List<ServiceInstance> instances = discoveryClient.getInstances("inventory-service");
ServiceInstance instance = loadBalancer.choose(instances);
String url = instance.getUri() + "/api/stock/" + productId;Pros: No additional network hop, client controls load balancing strategy.
Cons: Discovery logic in every client; every language/framework needs a discovery client library.
Server-side discovery:
The client calls a load balancer or service mesh, which queries the registry and routes the request to a healthy instance. The client knows nothing about discovery.
Client ──▶ Load Balancer (knows about registry) ──▶ healthy instanceKubernetes DNS is server-side: http://inventory-service.default.svc.cluster.local resolves to a ClusterIP that kube-proxy load balances across pods.
Pros: Language-agnostic, centralized.
Cons: Extra network hop, load balancer is a potential bottleneck/failure point.
In practice: Kubernetes provides server-side discovery via DNS + kube-proxy. Service meshes (Istio, Linkerd) add richer load balancing (weighted, circuit-breaking-aware) via sidecar proxies.
26. How does Kubernetes service discovery work?
Answer:
Kubernetes uses DNS-based service discovery. When you create a Service object, the cluster DNS (CoreDNS) automatically creates an A record:
<service-name>.<namespace>.svc.cluster.localPods in the same namespace can reach the service at just . Cross-namespace requires the full FQDN.
apiVersion: v1
kind: Service
metadata:
name: inventory-service
namespace: production
spec:
selector:
app: inventory
ports:
- port: 8080
targetPort: 8080# From any pod in the production namespace:
curl http://inventory-service:8080/api/stock/123
# From a different namespace:
curl http://inventory-service.production.svc.cluster.local:8080/api/stock/123kube-proxy (or eBPF-based replacements like Cilium) maintains iptables/IPVS rules that forward requests to one of the pod IPs behind the Service, load-balancing across them.
Headless services (.spec.clusterIP: None) return all pod IPs in DNS, useful for stateful sets (Kafka, Cassandra) where you want to connect to a specific pod.
Part 10 — Distributed Tracing
27. Why is distributed tracing essential in microservices, and how does it work?
Answer:
In a monolith, a slow request shows up in a single thread stack trace. In microservices, a 3-second API response might involve 12 service calls. Logs from each service are fragmented across different log streams. You cannot correlate them without distributed tracing.
How it works:
Every request gets a trace ID at the entry point (API gateway or first service). Each service-to-service call creates a span — a timed unit of work with a parent-child relationship to the trace.
Trace ID: abc-123
│
├── Span: API Gateway (0–50ms)
│ ├── Span: Order Service (10–200ms)
│ │ ├── Span: DB query (10–30ms)
│ │ └── Span: Inventory Service call (50–180ms)
│ │ ├── Span: Cache lookup (50–60ms)
│ │ └── Span: DB query (70–180ms) ◀── THIS is slow
│ └── Span: Auth Service (5–20ms)The trace ID is propagated in HTTP headers (traceparent in W3C Trace Context standard, or X-B3-TraceId in B3 format).
// OpenTelemetry — automatic instrumentation adds trace context
// Manual span creation for custom operations
Span span = tracer.spanBuilder("inventory.checkStock")
.setAttribute("product.id", productId)
.setAttribute("requested.quantity", quantity)
.startSpan();
try (Scope scope = span.makeCurrent()) {
return inventoryRepository.findStock(productId);
} catch (Exception e) {
span.recordException(e);
span.setStatus(StatusCode.ERROR);
throw e;
} finally {
span.end();
}Popular backends: Jaeger, Zipkin, Honeycomb, Datadog APM, AWS X-Ray.
28. What is the difference between tracing, metrics, and logs? (The three pillars of observability.)
Answer:
| Pillar | What it tells you | Examples | Tools |
|---|---|---|---|
| Metrics | Aggregated numerical measurements over time | requests/second, p99 latency, error rate, CPU% | Prometheus, Datadog, CloudWatch |
| Logs | Timestamped discrete events with context | "OrderPlaced orderId=123 userId=456 total=99.00" | ELK, Loki, Papertrail |
| Traces | End-to-end request flow across services | Which service was slow, which DB query ran | Jaeger, Zipkin, Honeycomb |
How they work together: An alert fires because p99 latency exceeded 2s (metrics). You look at traces for that time window to find which service is slow. You look at logs from that service to find the specific error or slow query.
Structured logging is the bridge between logs and traces: log every request with its trace ID so you can pivot from a trace to the raw log lines.
{
"timestamp": "2024-01-15T10:23:45Z",
"level": "ERROR",
"service": "inventory-service",
"traceId": "abc-123-def-456",
"spanId": "789-xyz",
"message": "Stock reservation failed",
"productId": "sku-789",
"requestedQty": 5,
"availableQty": 2
}Part 11 — Database per Service
29. Why does each microservice need its own database?
Answer:
A shared database is the most common cause of the distributed monolith anti-pattern. If three services all read and write to the same PostgreSQL schema:
- A schema migration needed by Service A requires coordinating with Teams B and C.
- A slow query from Service B locks tables that Service A depends on.
- Services cannot be deployed independently because the schema is shared.
- Services cannot choose different databases optimized for their access patterns.
Database per service means:
- Service A owns its schema exclusively. No other service queries its tables directly.
- Service A can migrate its schema on its own release schedule.
- Service A can choose PostgreSQL, MongoDB, Redis, or Cassandra based on its data model and access patterns.
How services share data: Through APIs (synchronous) or events (asynchronous), never through direct database access.
30. How do you handle queries that need to join data across service databases?
Answer:
This is one of the top practical challenges in microservices. You cannot do SELECT o.*, u.name FROM orders o JOIN users u ON o.user_id = u.id when orders and users are in separate databases.
Pattern 1 — API composition:
The client (or a BFF) calls both services and joins the results in application code.
async function getOrdersWithUserInfo(userId: string) {
const [orders, user] = await Promise.all([
orderService.getOrdersByUser(userId),
userService.getUser(userId),
]);
return orders.map(order => ({ ...order, userName: user.name }));
}Works well for simple cases. Becomes inefficient for queries with complex filtering across both datasets.
Pattern 2 — CQRS read model / materialized view:
A dedicated read service subscribes to events from both Order and User services and maintains a denormalized orders_view table with all the data needed for the query.
-- orders_view maintained by a projection consuming OrderPlaced + UserUpdated events
CREATE TABLE orders_view (
order_id UUID PRIMARY KEY,
user_id UUID,
user_name TEXT, -- denormalized from User service
user_email TEXT, -- denormalized from User service
total NUMERIC,
status TEXT,
created_at TIMESTAMP
);This is the correct solution for complex queries. The cost is eventual consistency and maintaining the projection.
Pattern 3 — Dedicated reporting database:
Use an ETL pipeline (Debezium + Kafka + a data warehouse) to aggregate data from multiple service databases into a single analytical store for complex cross-service queries.
31. What is the saga vs two-phase commit trade-off for distributed transactions?
Answer:
Two-phase commit (2PC):
A distributed transaction coordinator asks all participants to "prepare" (phase 1), then if all agree, sends "commit" (phase 2).
Problems:
- Blocking protocol: If the coordinator crashes after phase 1, participants are locked waiting forever.
- Tight coupling: All participants must be available and implement the 2PC protocol.
- Does not work across different databases (PostgreSQL and MongoDB cannot participate in the same 2PC).
- Performance: Each transaction requires two round trips to all participants.
Saga:
- Non-blocking: Each step is a local transaction. No participant is blocked waiting for others.
- Loose coupling: Services communicate via events; they do not need to know about the transaction coordinator's internals.
- Works across heterogeneous stores: Each service uses whatever database it wants.
- Trade-off: Consistency is eventual, not immediate. Compensating transactions are harder to design correctly than rollbacks.
When 2PC is acceptable: Within a single service that uses multiple databases (e.g., PostgreSQL + Redis within the same bounded context). Never across service boundaries in a microservices architecture.
Part 12 — Strangler Fig Migration
32. Explain the strangler fig pattern for migrating a monolith.
Answer:
The strangler fig tree grows around a host tree and eventually replaces it. The pattern: incrementally build new microservices around the monolith, routing traffic to the new services as they are ready, until the monolith can be decommissioned.
Steps:
- 1Put a facade in front of the monolith. An API gateway or reverse proxy that currently routes everything to the monolith.
- 2Identify the first slice to extract. Pick a bounded context that has stable interfaces and is painful to work on in the monolith (high change frequency, scaling problems, team friction).
- 3Build the new service alongside the monolith. Do not touch the monolith yet.
- 4Route traffic to the new service. Update the facade to route requests for that domain to the new service. The monolith still handles everything else.
- 5Remove the old code from the monolith once the new service is proven stable.
- 6Repeat until the monolith is empty.
Phase 1: [Client] ──▶ [Facade] ──▶ [Monolith (all)]
Phase 2: [Client] ──▶ [Facade] ──▶ [Monolith (minus payments)]
└──▶ [Payment Service (new)]
Phase 3: [Client] ──▶ [Facade] ──▶ [Monolith (minus payments, inventory)]
├──▶ [Payment Service]
└──▶ [Inventory Service (new)]33. What are the risks of a strangler fig migration and how do you mitigate them?
Answer:
Risk 1 — Data synchronization during transition:
While both monolith and new service are live, they may each write to data. If the monolith's database and the new service's database diverge, you have a split-brain problem.
Mitigation: Use the branch by abstraction approach within the monolith — wrap the feature behind an interface, implement both the old and new versions, use a feature flag to switch between them.
Or: keep the new service writing back to the monolith's database initially (via the old API), then migrate the data store as a separate later step.
Risk 2 — The strangler fig stops halfway:
Teams extract 2–3 services, the monolith is reduced but not eliminated, and then business pressure stops the migration. Now you have a distributed monolith.
Mitigation: Make the migration a first-class product goal with executive commitment. Track the percentage of traffic handled by new services as a KPI. Set a deprecation date for each monolith module.
Risk 3 — Hidden coupling revealed during extraction:
When you try to extract the Notification service, you discover it reads 15 tables from the monolith's database.
Mitigation: Run the domain model analysis before committing to extract a service. Use static analysis tools to find cross-domain table accesses.
34. How do you handle database migration during strangler fig?
Answer:
Option A — Keep using the monolith's database initially:
The new service connects to the monolith's database (only its logical tables). This is a temporary coupling, but it avoids the data synchronization problem during transition.
Option B — Dual-write with sync:
Both the monolith and the new service write to their respective databases. A synchronization job keeps them in sync. This is complex and error-prone — use it only when necessary.
Option C — Expand/contract (preferred for schema changes):
- 1Expand: Add new columns/tables to the monolith's schema without removing old ones. New service writes to new schema; monolith still reads old schema.
- 2Migrate data: Backfill new columns, update application code in both old and new service to use new schema.
- 3Contract: Remove old columns/tables once nothing reads them.
This allows zero-downtime schema migration with multiple services in transition.
35. If you had to start a new greenfield project, when would you start with microservices vs a monolith?
Answer:
Start with a monolith (specifically, a modular monolith):
A greenfield project has undiscovered domain boundaries. Splitting too early creates expensive-to-fix service boundaries that cut across the wrong lines. Sam Newman (author of "Building Microservices") and Martin Fowler both recommend starting with a monolith for new projects.
The modular monolith gives you the organizational and code benefits of clear boundaries without the operational overhead:
- Enforce strict module boundaries in code (separate packages/modules that cannot cross-import arbitrarily).
- Each module owns its data (separate schema prefixes or separate tables).
- Each module has a public API used by other modules, not direct database access.
When to start extracting services:
- When a specific module has scaling requirements that differ from the rest.
- When a team needs to deploy independently.
- When a module needs a fundamentally different technology.
- When the business domain is well understood (you've been running the product for 6–12 months).
The modular monolith is not a failure mode — it is the strangler-fig source tree that you extract from deliberately, not urgently.
Practical Tips for the Interview
What separates senior answers from mid-level answers:
- 1Trade-offs, not just benefits. Every pattern has a cost. Name both.
- 2Operational reality. Mention what happens at 3 AM when this fails in production. Who gets paged? How is it diagnosed?
- 3Sizing judgment. You do not always need microservices. Saying "it depends" and then articulating the dependencies is a strong signal.
- 4Concrete numbers. Kafka retention periods, circuit breaker thresholds, connection pool sizes — interviewers notice when you have actually configured these things.
Common mistakes to avoid:
- Saying "just use Kubernetes and it handles all of this" — Kubernetes solves deployment, not architectural problems.
- Conflating event sourcing with event-driven architecture. They are different things.
- Claiming sagas solve consistency problems "automatically" — compensating transactions require careful design.
- Ignoring the network. Every service call can fail, be slow, or return stale data. Design for this explicitly.
How to structure your answers in the interview:
- 1State the core concept in one sentence.
- 2Give a concrete example from a real system (or a realistic scenario).
- 3Name the main trade-off or failure mode.
- 4Describe how you'd mitigate it in production.
This structure shows depth without rambling.