Build a Retry Engine with Exponential Backoff
Difficulty: Easy · Category: Retry Pattern · Languages: Java 17, Python 3.11, Go 1.22, Node.js 20, TypeScript 5.x
Retry Engine with Exponential Backoff
Background
Network calls fail. Databases go down temporarily. External APIs hit rate limits. In production systems, transient failures are the norm, not the exception. A well-designed retry engine handles these gracefully by retrying failed operations with increasing delays (exponential backoff) and randomized jitter to prevent the "thundering herd" problem.
Libraries like Resilience4j, Spring Retry, and Polly all implement this pattern. In this problem you will build your own from scratch.
Your Task
Implement the RetryEngineContract interface. Your implementation must:
- Support configurable retry parameters: max retries, initial delay, backoff multiplier, and max delay cap.
- Implement exponential backoff: each retry delay is multiplied by the backoff factor, up to the max delay cap.
- Add jitter (±10% randomization) to each delay to prevent thundering herd.
- Track per-operation state: attempt count, status, last error, and timestamps of each attempt.
- Support cancellation and reset of operations.
Recommended infrastructure:
kv-store— for tracking retry state and attempt history, but you may use in-memory structures.
How It Works
The platform injects your chosen infrastructure SDK(s) via constructor. For example, if you use the kv-store:
public class MyRetryEngine implements RetryEngineContract {
private final KVStoreClient kvStore;
public MyRetryEngine(KVStoreClient kvStore) {
this.kvStore = kvStore;
}
// ... implement methods using kvStore.put(), kvStore.get(), etc.
}
See the SDK Docs tab for the full API reference of your chosen system.
Interface
public interface RetryEngineContract {
void configure(int maxRetries, long initialDelayMs, double backoffMultiplier, long maxDelayMs);
boolean execute(String operationId, Callable<Boolean> action);
int getAttemptCount(String operationId);
String getStatus(String operationId);
String getLastError(String operationId);
long getNextRetryDelayMs(String operationId);
List<Long> getRetryTimestamps(String operationId);
void cancel(String operationId);
void reset(String operationId);
}
Backoff Formula
delay(n) = min(initialDelayMs × multiplier^(n-1), maxDelayMs)
actualDelay = delay(n) × (1 + jitter) where jitter ∈ [-0.10, +0.10]
Status Lifecycle
PENDING → RETRYING → SUCCEEDED
→ FAILED
→ CANCELLED
Constraints
maxRetries ≥ 0(0 = execute once, no retries)initialDelayMs ≥ 1backoffMultiplier ≥ 1.0maxDelayMs ≥ initialDelayMsoperationIdis non-null and non-empty- Thread-safe: concurrent operations must not interfere
Examples
configure(3, 100, 2.0, 5000)
// Action succeeds on attempt 3:
execute("op-1", () -> attemptCount++ >= 2) → true
getAttemptCount("op-1") → 3
getStatus("op-1") → "SUCCEEDED"
getNextRetryDelayMs("op-1") → 0 (completed)
// Action always fails:
execute("op-2", () -> { throw new Exception("timeout"); }) → false
getAttemptCount("op-2") → 4 (1 + 3 retries)
getStatus("op-2") → "FAILED"
getLastError("op-2") → "timeout"
Hints
- Use
Thread.sleep()for delays between retries. For testing, delays will be very short (10–100 ms). - Use
ThreadLocalRandom.current().nextDouble(-0.10, 0.10)for jitter. - In-memory approach: Store per-operation state in a
ConcurrentHashMap<String, OperationState>. - SPI-based approach: Use the
kv-storeto persist operation state, which lets you track retries across method calls cleanly. - Record
System.currentTimeMillis()before each attempt. - Check for cancellation between retries.
Infrastructure your code runs against
- Key-Value Store
How your solution is graded
Your score combines functional correctness (40%), performance (40%) and code quality (20%).
- Target throughput: 5,000 operations/second
- Target p99 latency: 15 ms
- Load test concurrency: 4 concurrent clients
- Load test duration: 10 seconds
- Maximum error rate: 0.5%