Approach: Use an Idempotency-Key header to look up a store (e.g., Redis or an in-memory ConcurrentHashMap for demo) that holds an entry with response data, status, and a lock/future to coordinate concurrent requests. On first request, acquire a single-writer token (via computeIfAbsent or a distributed lock), execute the handler, save the serialized response and TTL, then return it. Concurrent duplicates wait for the result and return the saved response.java
// Pseudocode / simplified Java
class IdempotencyEntry {
volatile CompletableFuture<Response> future = new CompletableFuture<>();
long expiresAt;
}
class IdempotencyService {
ConcurrentMap<String, IdempotencyEntry> store = new ConcurrentHashMap<>();
Duration ttl = Duration.ofMinutes(10);
Response handleRequest(Request req, Function<Request, Response> handler) {
String key = req.getHeader("Idempotency-Key");
if (key == null || key.isBlank()) return handler.apply(req); // or reject
// Try to create entry atomically if absent
IdempotencyEntry entry = store.compute(key, (k, existing) -> {
long now = System.currentTimeMillis();
if (existing == null || existing.expiresAt <= now) {
IdempotencyEntry e = new IdempotencyEntry();
e.expiresAt = now + ttl.toMillis();
return e;
}
return existing;
});
// If this request created the future (not completed), caller computes
boolean shouldCompute = !entry.future.isDone();
if (shouldCompute) {
try {
Response resp = handler.apply(req); // execute business logic
entry.future.complete(resp); // publish result
// Optionally store serialized response and status in Redis with TTL
return resp;
} catch (Throwable t) {
entry.future.completeExceptionally(t); // propagate error to waiters
store.remove(key); // allow retry on failure
throw t;
}
} else {
// Wait for the original computation to finish and return same response
try {
return entry.future.get(); // can add timeout
} catch (ExecutionException ee) {
throw new RuntimeException(ee.getCause());
}
}
}
// Background cleanup (if using in-memory store)
void cleanupExpired() {
long now = System.currentTimeMillis();
store.entrySet().removeIf(e -> e.getValue().expiresAt <= now);
}
}
Key points:- Use compute/atomic operations to avoid race conditions.- Store serialized response + status so duplicates return identical payload/headers.- Set TTL in backing store (Redis EXPIRE recommended) to avoid unbounded growth.- On handler failure, remove entry to allow retry; on success keep entry until TTL.- For distributed systems use Redis SETNX + Lua or a dedicated idempotency table with transactions.