Microservices work well when everything works. But production systems are rarely that simple.
A downstream service becomes slow. A database connection pool gets exhausted. A Kafka consumer crashes halfway through processing a message. A Kubernetes Pod keeps restarting. Network latency suddenly increases.
And one small failure can quickly become a system-wide outage.
This is why senior Java and Spring Boot interviews often focus less on “How do you build a microservice?” and more on:
“What happens when the microservice fails?”
In this article, we cover 40 real-world microservices failure scenarios and interview questions, followed by a senior-level cascading failure scenario.
The goal is not to memorize definitions. The goal is to understand how a production system behaves under failure and how you would design it to recover safely.
Consider a simple request flow:
Service A → Service B → Service C → Database
If Service C becomes slow, Service B may start waiting for responses.
If Service B's threads become blocked, Service A may also start waiting.
Eventually, Service A can run out of threads or other resources and become unavailable.
Service C may have started the problem, but Service A is where users finally see the outage.
This is the essence of a cascading failure.
Senior engineers need to understand not only how to fix the original problem, but also how to prevent one failing dependency from taking down the entire system.
If Service A calls Service B synchronously and Service B becomes unavailable, requests from Service A may fail, timeout, or remain blocked while waiting for a connection.
If there are enough requests, Service A can eventually exhaust its own resources.
A resilient design should use appropriate timeouts, circuit breakers, bulkheads, fallbacks, and controlled retries where appropriate.
The most important techniques include:
The objective is to isolate failures so that one unhealthy dependency does not consume all the resources of its callers.
A slow service can be more dangerous than an immediately unavailable service.
For example:
Service B response time increases from 100 ms to 10 seconds.
If Service A has hundreds of concurrent requests waiting for Service B, its threads, connections, and memory can become occupied.
Eventually, Service A may become slow even though its own code is functioning correctly.
This is why every remote call should have a carefully chosen timeout.
Timeouts should be based on the expected behavior of the dependency and the user-facing latency requirement.
Do not simply configure every service with a 30-second timeout.
For example, if the complete API must respond within 2 seconds, allowing one downstream call to wait for 30 seconds makes little sense.
Consider:
A retry attempts an operation again when a failure may be temporary.
A circuit breaker prevents calls to a dependency when it is consistently unhealthy.
Retries are useful for transient failures.
Circuit breakers are useful for preventing repeated calls from overwhelming an unhealthy dependency.
They solve different problems and are often used together.
Imagine 1,000 requests are sent to a failing service.
If every request retries three times, the dependency could receive thousands of additional requests while already struggling.
This can create a retry storm.
Use limited retries, exponential backoff, jitter, and circuit breakers where appropriate.
The Bulkhead pattern isolates resources so that failure in one dependency does not consume everything available to the application.
For example, imagine an application has separate resource limits for:
If the notification service becomes slow, it should not be allowed to consume all resources required for order processing.
Distributed systems frequently experience partial failures.
One service may work while another does not.
The application should decide what can safely continue without the failed dependency.
Possible strategies include:
An idempotent API produces the same business result even if the same request is processed more than once.
For operations such as payments or order creation, a common approach is to use an idempotency key.
The server stores the result associated with the key and prevents duplicate processing.
This can happen because of client retries, network failures, message redelivery, or timeout ambiguity.
For example:
Client → Create Order → Server creates order → Network failure → Client retries
The client does not know whether the first request succeeded.
Without idempotency, two orders may be created.
A unique request identifier and database constraint can help prevent this.
Distributed messaging systems can deliver a message more than once.
Consumers should therefore be designed to tolerate duplicate delivery when required.
Common approaches include:
The key principle is:
Do not assume that a message will always be delivered exactly once.
The behavior depends on when the message offset was committed.
If processing happens before the offset is safely committed and the consumer crashes, the message may be delivered again.
This is why consumers need to be designed with appropriate acknowledgment and idempotency strategies.
A poison message is a message that repeatedly fails processing.
If the consumer continuously retries it, it can block progress and consume resources.
A common approach is to limit retries and eventually move the message to a Dead Letter Queue for investigation.
A Dead Letter Queue, or DLQ, is a separate destination for messages that could not be processed successfully after the configured retry policy.
It allows the main consumer flow to continue while preserving failed messages for debugging or later recovery.
Message ordering should be treated as a business requirement rather than an assumption.
For example, an order might require:
Order Created → Payment Completed → Order Shipped
Processing these events out of order could create an inconsistent state.
Partitioning strategy, keys, consumer design, and event modeling all influence ordering.
Requests requiring the database will begin failing or timing out.
If the application holds threads and connections while waiting for the database, resource exhaustion can follow.
The application should use:
First determine why the connections are being consumed.
Possible causes include:
Increasing the pool size without understanding the root cause can make the database problem worse.
The JVM may eventually throw an OutOfMemoryError and the process can terminate.
In Kubernetes, the container may then be restarted depending on the circumstances and configuration.
The real investigation should determine why memory grew:
Start by confirming the CPU increase and identifying whether it is application CPU or GC-related.
Then investigate:
The objective is to identify what is consuming CPU rather than simply adding more CPU.
Repeated restarts may indicate:
Check Pod events, container logs, exit codes, resource metrics, and probe configuration.
Readiness determines whether a Pod should receive traffic.
Liveness helps determine whether the application is still functioning sufficiently to remain running.
A poorly configured probe can cause healthy applications to be removed from service or repeatedly restarted.
Probes should therefore represent meaningful application health conditions.
If service discovery becomes unavailable, services may be unable to locate new instances or refresh endpoint information.
Depending on the platform, clients may have cached service information.
Resilience can include:
If there is only one gateway instance, it can become a single point of failure.
Clients may lose access to the entire backend even though individual microservices are healthy.
The gateway therefore needs redundancy and an appropriate traffic-distribution strategy.
A typical production architecture places multiple gateway instances behind a load balancer.
For example:
Clients → Load Balancer → Gateway Instances → Microservices
If one gateway instance fails, traffic can be routed to healthy instances.
Instead of trying to maintain one traditional database transaction across multiple microservices, many systems use patterns such as the Saga pattern.
A business transaction is divided into local transactions, with compensating actions used when later steps fail.
The system needs to execute the appropriate recovery or compensating action.
For example:
Create Order → Reserve Inventory → Charge Payment
If payment fails, the system may need to release the inventory reservation and mark the order appropriately.
This requires careful state management and idempotent operations.
Choreography allows services to react to events without a central coordinator.
Orchestration uses a central component to coordinate the workflow.
Choreography can be simple for small event-driven workflows.
Orchestration can make complex business workflows easier to understand and control.
The choice depends on workflow complexity, ownership, failure handling, and operational requirements.
In distributed systems, different services may not see the same state immediately.
The system should explicitly model this behavior.
Useful techniques include:
Business requirements should define how much temporary inconsistency is acceptable.
Remote calls take longer.
That means more threads and connections may remain occupied while waiting.
As concurrency increases, resource exhaustion can follow.
This is why timeouts, bounded concurrency, circuit breakers, and monitoring of network latency are important.
Intermittent failures are often more difficult than complete outages.
I would correlate:
Distributed tracing is especially useful because it can show which dependency failed for individual requests.
Use distributed tracing with a trace context that propagates between services.
A typical request might look like:
Gateway → Order Service → Payment Service → Database
The trace groups the operations belonging to the same request while individual spans represent individual operations.
Look at the distributed trace and compare the duration of each service and downstream operation.
For example:
| Component | Duration |
|---|---|
| API Gateway | 50 ms |
| Service A | 100 ms |
| Service B | 4,700 ms |
| Database | 4,300 ms |
The trace immediately narrows the investigation.
Missing logs can make production troubleshooting extremely difficult.
A strong observability strategy should not depend on logs alone.
Use metrics and traces as additional sources of evidence.
Structured logs with correlation or trace identifiers also make it easier to connect logs to specific requests.
First determine whether the system can scale horizontally and whether its dependencies can handle the increased load.
Possible techniques include:
Scaling the application without scaling or protecting its database can simply move the bottleneck downstream.
This is a common multi-tenant reliability problem.
One tenant generating extreme traffic should not be able to exhaust shared resources.
Possible controls include:
Use deployment strategies such as rolling deployments or blue-green deployments.
The key requirements are:
The old version should remain available until the new version is healthy enough to receive traffic.
A rollback should be quick and predictable.
Good deployment practices include:
Rollback should be part of the deployment design, not an emergency procedure invented during an outage.
Services should not assume that every consumer can upgrade simultaneously.
Use compatible API changes, versioning where necessary, tolerant readers, and carefully managed event schemas.
A common strategy is:
Add new field → Deploy consumers → Migrate usage → Remove old field later
This reduces the risk of breaking independently deployed services.
I would design the service around the assumption that dependencies will eventually fail.
The design would consider:
The service should also define what functionality can continue when a dependency is unavailable.
Start with a request or order identifier.
Trace the complete flow:
Client → Gateway → Order → Inventory → Payment → Database → Events
Then correlate:
Look for patterns.
Does the failure happen only under high traffic? Only for one service? Only for certain tenants? Only when a particular dependency becomes slow?
Random failures usually become less random once the right telemetry is correlated.
Service C becomes slow.
Soon Service B starts timing out, then Service A becomes unavailable.
What caused the failure?
How would you prevent this cascading failure?
Service A depends on Service B.
Service B depends on Service C.
Service C depends on the database.
The failure originated in the database or Service C, but the impact propagated upstream.
When Service C becomes slow, Service B's requests remain active for longer.
That can consume:
Eventually Service B cannot process new requests efficiently.
The chain may look like this:
Database slowdown
↓
Service C latency increases
↓
Service B waits longer
↓
Service B resources become exhausted
↓
Service A requests start timing out
↓
Service A becomes unavailable
Service B should not wait indefinitely for Service C.
A timeout limits how long resources can remain occupied by a failed dependency.
If Service C is consistently unhealthy, Service B should temporarily stop sending unnecessary requests to it.
This prevents Service B from continuously consuming resources while Service C is recovering.
Resources used for Service C should be isolated where appropriate.
Service C should not be able to consume every available thread or connection in Service B.
Retries should be limited and use appropriate backoff.
Otherwise, retries can amplify the original outage.
Metrics should show the increase in latency and errors.
Distributed traces should show where time is being spent.
Logs should provide detailed information about failures.
With proper observability, the chain becomes visible:
Database latency ↑ → Service C latency ↑ → Service B timeout ↑ → Service A latency ↑
Ask an important business question:
Does Service A really need Service C for every request?
If not, cached or partial responses may allow some functionality to continue while the dependency recovers.
Microservices are not successful simply because services are separated into independent deployments.
The real challenge is handling the failures between those services.
A senior engineer should think about:
These questions should be answered before production, not during an incident.
| Failure | Useful Strategy |
|---|---|
| Slow downstream service | Timeout + Circuit Breaker |
| Transient failure | Limited Retry + Backoff |
| Resource exhaustion | Bulkhead + Bounded Concurrency |
| Duplicate request | Idempotency Key |
| Duplicate message | Idempotent Consumer |
| Poison message | Retry + Dead Letter Queue |
| Database failure | Timeout + Graceful Failure |
| Traffic spike | Autoscaling + Rate Limiting |
| Pod failure | Health Checks + Restart |
| Deployment failure | Rollback Strategy |
| Cross-service debugging | Metrics + Logs + Traces |
The biggest difference between a basic microservices implementation and a production-ready microservices architecture is how the system behaves when something goes wrong.
Dependencies will become slow.
Networks will fail.
Databases will become overloaded.
Messages will be duplicated.
Pods will crash.
Deployments will occasionally go wrong.
The goal is not to build a system where failures never happen.
The goal is to build a system where failures are isolated, detected quickly, and recovered safely.
For senior Java and Spring Boot developers, this is one of the most important microservices interview topics to understand.
Don't just learn how microservices communicate.
Learn how they fail.
If you are preparing for senior Java, Spring Boot, Microservices, AWS, or Kubernetes interviews, explore more practical backend engineering guides from LogicBrace.
A cascading failure occurs when one failing or slow service causes dependent services to consume resources waiting for it, eventually causing failures to propagate through the system.
Timeouts, circuit breakers, bulkheads, controlled retries, rate limiting, bounded concurrency, graceful degradation, asynchronous processing, and strong observability can help prevent cascading failures.
Without timeouts, application resources can remain occupied while waiting for an unhealthy dependency. This can eventually exhaust threads, connections, memory, or other resources.
Yes. Excessive retries can create a retry storm and increase load on an already unhealthy service. Retries should generally be limited and use appropriate backoff and jitter.
Network failures, client retries, and message redelivery can cause the same operation to be processed more than once. Idempotency helps ensure duplicate processing does not create incorrect business results.
Distributed tracing follows a request across service boundaries and helps identify where latency or errors were introduced, making it much easier to locate the failing dependency.
Instead of memorizing definitions, practice explaining real production scenarios. Be prepared to discuss failure isolation, retries, timeouts, circuit breakers, Kafka failures, database failures, Kubernetes behavior, observability, and rollback strategies.
0 Comments