Design a Retry Mechanism with Exponential Backoff
Fast recall
Quick revision
Core answer
Random.nextLong(0, computedDelay)) to break synchronized retry storms. Cap max attempts (~3-5) and total elapsed time (so you don't retry past the user's patience). Classify errors before retrying: transient (timeout, connection refused, 502/503/504) → retry; fatal (400/401/403/422, business validation) → don't retry. Pair retries with idempotency keys for non-GET operations. Resilience4j Retry module handles all this; rolling your own is a one-shot exercise.Backoff math (exponential + jitter), error classification, idempotency awareness, and budget enforcement (max attempts + max total time).
Constant-delay retries (creates retry storms), retrying 4xx errors (will never succeed), or retrying non-idempotent writes without idempotency keys (duplicate side effects).
Discuss full vs equal vs decorrelated jitter, retry budgets at the system level (Google SRE pattern), and the interaction between retry and circuit breaker.
Interview-ready guide
Interview answer
What a retry must do
Naive for (int i = 0; i < 3; i++) has caused more outages than it's prevented. Proper retry needs: bounded attempts (3-5), bounded total time, backoff between attempts, jitter, error classification, idempotency awareness, observability, and coordination with circuit breaker.
Backoff math
Constant delay is the anti-pattern — 1000 clients all retry at the same instant, downstream gets re-hammered.
Exponential: base × 2^attempt, capped at maxDelay (60s typical). 100ms, 200ms, 400ms, 800ms.
Jitter is the critical addition. Three variants from the AWS architecture blog:
- Full jitter:
random(0, computed)— maximum spread, safe default - Equal jitter:
computed/2 + random(0, computed/2)— less spread - Decorrelated:
random(base, prevDelay * 3)— smoother distribution, no direct attempt-count dependence
Without jitter, synchronized retries are guaranteed to re-overload a recovering downstream.
Error classification — don't retry what won't succeed
Retry: IOException, SocketTimeoutException, ConnectException, HTTP 5xx (502/503/504), 429 (honor Retry-After), 408, some database deadlocks.
Don't retry: 4xx (400/401/403/404/422), business validation, IllegalArgumentException, NullPointerException, auth failures (unless token-refresh handles them).
Case-by-case: 500 — might be transient or persistent; retry with caution.
Idempotency — the hidden requirement
The danger: retry a POST /charge after timeout. Did the first call succeed? You don't know. Retry causes a double charge.
Solution: idempotency keys. Client generates a UUID per logical operation; same key on every retry of the same operation. Server caches result under this key; subsequent requests return cached result without re-execution.
Mandatory for non-idempotent writes (POST, PATCH, state-change DELETE). Naturally idempotent operations (GET, PUT, idempotent DELETE) don't need keys.
Resilience4j implementation
RetryConfig.custom()
.maxAttempts(4)
.intervalFunction(IntervalFunction.ofExponentialRandomBackoff(
Duration.ofMillis(100), 2.0, 0.5))
.retryOnException(this::isRetryable)
.ignoreExceptions(IllegalArgumentException.class, AuthException.class)
.build();With TimeLimiter for total deadline.
Hand-rolled minimum
for (int attempt = 0; attempt < maxAttempts; attempt++) {
try { return supplier.get(); }
catch (Throwable t) {
if (!retryable.test(t)) throw t;
if (attempt == maxAttempts - 1) break;
long delay = computeBackoffWithJitter(attempt);
if (System.nanoTime() + ms2ns(delay) > deadline) break;
sleep(delay);
}
}
throw lastException;Deadline check before each sleep; full-jitter via random.nextLong(exp + 1).
Retry budget — system-level concern
Per-call budget isn't enough. 10K req/sec, 50% failing, 3 retries each = 25K req/sec hitting the broken downstream. Retries make recovery harder.
Google SRE pattern: cap retry rate at 10% of success rate. Token-bucket style — each successful response refills retry tokens; each retry consumes one. When tokens depleted, no more retries.
Pair with circuit breaker: when breaker is OPEN, retries are pointless and harmful; most libraries compose so OPEN short-circuits retry.
Common mistakes
- No jitter — thundering herd guaranteed
- Retrying 4xx — will never succeed
- No max total time — retries past user's patience
- Non-idempotent writes without keys — double-charges
- Retry inside retry inside retry — 9 attempts unintended
- No observability — you don't know retry rate is 80%
- Retry on known-fast operations — in-process calls don't need retry
Recommendation
Use Resilience4j Retry module: max 3-5 attempts, exponential backoff with full jitter (initial 100-200ms, multiplier 2, max 60s), bounded by total deadline via TimeLimiter, retryOnException matching only transient categories, ignoreExceptions for business/4xx errors. Pair every retried write with an idempotency key generated client-side. Compose with the circuit breaker so a tripped breaker short-circuits retries. Track retry rate as a downstream-health signal. At system scale, layer a token-bucket retry budget so retry storms can't make a recovering service's life worse.
Design a Retry Mechanism with Exponential Backoff
Direct Answer
Compute delay as min(maxDelay, base * 2^attempt) then add jitter (typically full jitter: Random.nextLong(0, computedDelay)) to break synchronized retry storms. Cap max attempts (~3-5) and total elapsed time (so you don't retry past the user's patience). Classify errors before retrying: transient (timeout, connection refused, 502/503/504) → retry; fatal (400/401/403/422, business validation) → don't retry. Pair retries with idempotency keys for non-GET operations. Resilience4j Retry module handles all this; rolling your own is a one-shot exercise.
Common Mistakes & Pitfalls
Constant-delay retries (creates retry storms), retrying 4xx errors (will never succeed), or retrying non-idempotent writes without idempotency keys (duplicate side effects).
Check your understanding
Answer these without looking back at the article.
- Walk through the retry behavior of 1000 simultaneous clients seeing a 503 — with and without full jitter.
- How do you set retry parameters for a synchronous user-facing API vs an async background job?
- Compare full, equal, and decorrelated jitter — when does each shine?
- Design a retry budget for a service that handles 100K req/sec with a 90% baseline success rate.
Related interview prep categories
2Want a guided curriculum order?
These modules are also sequenced in our flagship backend interview track — open it when you prefer an ordered path instead of browsing by topic.