Sunday, 30 August 2026

Design Resilient Microservice

 Yes. Think of a resilient microservice as a service that continues working—or fails gracefully—even when Redis, Kafka, DB, or another service becomes slow or unavailable.

Simple architecture

                         Client
                           │
                           ↓
                    API Gateway
                           │
                           ↓
                    Load Balancer
                           │
              ┌────────────┼────────────┐
              ↓            ↓            ↓
          Service-1    Service-2    Service-3
              │
        ┌─────┼──────────────┐
        ↓     ↓              ↓
      Redis  Kafka           DB

Now let's understand each mechanism with a simple Order Service example.


1. Timeout ⏱️

Suppose Order Service calls Payment Service.

Order Service
     │
     │ Pay ₹1000
     ↓
Payment Service
     │
     │ ....... very slow
     │

Don't wait forever.

paymentClient.setTimeout(2 seconds);

If Payment Service doesn't respond within 2 seconds:

2 seconds
    ↓
TIMEOUT
    ↓
Return failure/fallback

Why?

Without timeout:

100 requests
     ↓
100 threads waiting
     ↓
Payment is slow
     ↓
Threads get exhausted
     ↓
Order Service also becomes unavailable ❌

Interview line:

"I always configure timeouts for downstream calls so a slow dependency doesn't consume my resources indefinitely."


2. Retry ๐Ÿ”„

Suppose the payment request fails because of a temporary network problem.

Order Service
     │
     ├── Request ──X──> Payment
     │
     ├── Retry ────────> Payment
     │
     └── Success ✅

But don't retry everything.

Retry makes sense for temporary failures such as:

Connection timeout
Temporary network error
HTTP 503

Be careful with:

Invalid card ❌
Insufficient balance ❌
Bad request ❌

Those won't become successful just because you retry.


3. Exponential Backoff ๐Ÿ“ˆ

Don't do:

Retry 1 → immediately
Retry 2 → immediately
Retry 3 → immediately

Instead:

Request
   ↓
Fail
   ↓
Wait 100 ms
   ↓
Retry
   ↓
Fail
   ↓
Wait 200 ms
   ↓
Retry
   ↓
Fail
   ↓
Wait 400 ms
   ↓
Retry

Conceptually:

100ms → 200ms → 400ms → 800ms → ...

Usually add jitter so thousands of services don't retry at exactly the same time.

Why?

Imagine Payment Service is overloaded.

If 10,000 requests fail and all retry immediately:

10,000 requests
      ↓
Payment overloaded
      ↓
10,000 retries
      ↓
More overload ๐Ÿ’ฅ

Backoff spreads those retries out.


4. Circuit Breaker ⚡

This is one of the most important resilience patterns.

Suppose Payment Service is completely down.

Without circuit breaker:

Request 1 → Payment ❌
Request 2 → Payment ❌
Request 3 → Payment ❌
Request 4 → Payment ❌
...
Request 10000 → Payment ❌

We're continuously hitting a dead service.

Circuit breaker says:

             Payment Service
                    ❌
                    │
        ┌───────────┴───────────┐
        ↓                       ↓
   CLOSED                   OPEN
   normal calls              BLOCK calls

After detecting repeated failures:

CLOSED
   ↓
many failures
   ↓
OPEN
   ↓
stop calling Payment

After some time:

OPEN
 ↓
HALF-OPEN
 ↓
try a few requests
 ↓
success → CLOSED
failure → OPEN

Simple analogy

Circuit breaker is like the electrical breaker in your house.

If something is continuously causing problems:

Don't keep sending electricity.
Cut it off temporarily.

5. Bulkhead ๐Ÿšข

Bulkhead comes from ships.

A ship has separate compartments so that if one compartment floods, the whole ship doesn't sink.

Same idea in microservices.

Suppose Order Service calls:

Payment
Inventory
Notification

Give each dependency separate resources.

Order Service
│
├── Payment Pool       20 threads
│
├── Inventory Pool     20 threads
│
└── Notification Pool  10 threads

Suppose Notification Service becomes extremely slow:

Notification ❌
     ↓
10 threads occupied

Payment still has:

20 independent threads
     ↓
Payment continues working ✅

Without bulkhead:

Notification becomes slow
       ↓
all threads occupied
       ↓
Payment also stops
       ↓
Order Service fails ❌

Bulkhead = isolate failures.


6. Rate Limiting ๐Ÿšฆ

Suppose your API normally handles:

1,000 requests/second

Suddenly a client sends:

100,000 requests/second

Your service can crash.

Rate limiting says:

Client
  │
  ↓
API Gateway
  │
  ├── 1,000 requests → ✅
  │
  └── remaining → 429 Too Many Requests

Example:

User A → max 100 requests/minute
User B → max 100 requests/minute

This protects your service from:

  • traffic spikes

  • accidental loops

  • abusive clients

  • overload


7. Idempotency ๐Ÿ”‘

This is very important for payments/orders.

Imagine client sends:

POST /payment
₹10,000

Payment succeeds.

But the response is lost because of a network problem.

Client thinks:

"Payment failed."

So it sends again.

Request 1 → Payment ₹10,000 → SUCCESS
                         ↓
                    response lost

Request 2 → Payment ₹10,000 → ???

Now customer could be charged twice. ๐Ÿ˜ฑ

Use an Idempotency Key:

Idempotency-Key: ABC123

First request:

ABC123 → charge ₹10,000
       → SUCCESS

Second request:

ABC123 → already processed
       → return previous result

So:

Same request
     ↓
Same idempotency key
     ↓
Process only once

8. Health Checks ❤️

Load balancer needs to know:

"Is this service healthy?"

Expose something like:

GET /health

Healthy:

{
  "status": "UP"
}

If Service-1 is unhealthy:

Load Balancer
     │
     ├── Service-1 ❌
     │
     ├── Service-2 ✅
     │
     └── Service-3 ✅

Load balancer stops sending traffic to Service-1.

There are commonly two concepts:

Liveness  → Is the application alive?

Readiness → Is the application ready to receive traffic?

For example, a service might be alive but unable to connect to its DB. It may be not ready to receive traffic.


9. Distributed Tracing ๐Ÿ”

Imagine one customer request travels through:

Client
  ↓
API Gateway
  ↓
Order Service
  ↓
Payment Service
  ↓
Kafka
  ↓
Notification Service

Customer says:

"My order took 8 seconds."

Which service caused the delay?

Distributed tracing gives the request a trace ID.

Trace ID: ABC123

API Gateway       50ms
     ↓
Order Service    100ms
     ↓
Payment Service  7500ms  ← ๐Ÿšจ
     ↓
Kafka             50ms

Now you immediately know:

Payment Service took 7.5 seconds.

Tools commonly used include OpenTelemetry with systems such as Jaeger, Zipkin, or commercial observability platforms.


10. Centralized Logging ๐Ÿ“

Imagine you have:

Service-1
Service-2
Service-3
Service-4
Service-5

If every service stores logs locally:

Server 1 → logs
Server 2 → logs
Server 3 → logs
...

Debugging becomes painful.

Instead:

Services
   │
   ├──── logs ────┐
   ├──── logs ────┤
   └──── logs ────┘
                  ↓
          Centralized Logging
                  ↓
             Search / Dashboard

For example:

2026-08-31 10:20:01
traceId=ABC123
service=OrderService
orderId=5001
message="Payment request failed"

You can search:

traceId = ABC123

and see the complete request journey.


Putting everything together

A resilient Order Service might look like this:

                         Client
                           │
                           ↓
                    ┌─────────────┐
                    │ API Gateway │
                    │ Rate Limit  │
                    └──────┬──────┘
                           ↓
                    Load Balancer
                           │
             ┌─────────────┼─────────────┐
             ↓             ↓             ↓
          Service-1     Service-2     Service-3
             │
             │
       ┌─────┼─────────────────┐
       ↓     ↓                 ↓
     Redis  Kafka              DB
       │
       └──────────────────────────────┐
                                      │
                    Resilience         │
                    ──────────         │
                    Timeout            │
                    Retry              │
                    Backoff            │
                    Circuit Breaker    │
                    Bulkhead           │
                    Idempotency        │
                                      │
                    Observability      │
                    ─────────────      │
                    Health Checks      │
                    Distributed Trace  │
                    Centralized Logs   │

๐ŸŽฏ Easy interview answer

If an interviewer asks:

"How would you design a resilient microservice?"

You can answer:

"I would put the service behind an API Gateway and Load Balancer, and run multiple service instances for high availability. For downstream dependencies like Redis, Kafka, and DB, I would use timeouts, limited retries with exponential backoff and jitter, circuit breakers, and bulkheads to prevent cascading failures. I would use rate limiting to protect the service from overload and idempotency keys for operations such as payments or order creation. I would also implement liveness and readiness health checks so unhealthy instances are removed from traffic. Finally, I would use distributed tracing and centralized logging with correlation or trace IDs so that failures can be diagnosed across multiple services."

๐Ÿง  Remember it as 3 layers

1️⃣ PROTECT
   Timeout
   Retry + Backoff
   Circuit Breaker
   Bulkhead
   Rate Limit

2️⃣ CORRECTNESS
   Idempotency

3️⃣ OBSERVABILITY
   Health Check
   Distributed Tracing
   Centralized Logging

One sentence to remember:

Protect the service → prevent duplicate/wrong operations → make failures visible.

No comments:

Post a Comment