Let me start with the confusion that prompted this post: in a recent mentoring session, a senior engineer described their payment service as "idempotent" because it had exponential backoff on retries. That's a category error and it's the kind of mistake that bites teams in production with real financial consequences.
Retry and idempotency are complementary, not synonymous.
Two Different Actors, Two Different Concerns
Retry logic lives in the client. It's the client's decision to resend a request after a failure or timeout.
Idempotency lives in the server. It's the server's guarantee that receiving the same request multiple times will have the same effect as receiving it once.
These are independent properties. Here's what each combination looks like:
Retries without server idempotency: every retry is a new operation. Network timeout on a payment? Retry charges the customer again. This is the double-charge bug.
Server idempotency without client retries: the server is prepared for duplicates, but the client gives up after one timeout. The payment succeeded, the UI says error. Support tickets follow.
Neither: timeouts result in unknown state, retries cause duplicates, correctness depends on luck.
Both, correctly implemented: the client retries safely, the server deduplicates, the user sees a correct result. This is the design you want.
What Good Retry Logic Looks Like
int attempt = 0;
while (attempt < MAX_ATTEMPTS) {
try {
return paymentClient.submit(request, idempotencyKey);
} catch (TimeoutException | ServiceUnavailableException e) {
long delay = BASE_DELAY_MS * (1L << attempt) + random.nextInt(JITTER_MS);
Thread.sleep(delay);
attempt++;
}
}
throw new PaymentSubmissionFailedException("Max retries exceeded");
The critical detail: the same idempotency key on every retry attempt. If you're generating a new idempotency key on each retry, you've broken the connection between retry and idempotency. Each retry looks like a new request to the server.
What Good Server-Side Idempotency Looks Like
public PaymentResponse processPayment(String idempotencyKey, PaymentRequest request) {
try (Lock lock = distributedLock.acquire(idempotencyKey)) {
Optional<PaymentResponse> stored = idempotencyStore.get(idempotencyKey);
if (stored.isPresent()) {
return stored.get();
}
PaymentResponse response = paymentGateway.charge(request);
idempotencyStore.save(idempotencyKey, response, TTL_24H);
return response;
}
}
Two non-obvious requirements here:
Distributed lock: without a lock, two concurrent requests with the same key can both pass the "not found" check and both execute the payment.
Durable storage: the idempotency store must survive restarts. A cache with eviction can lose a key making a stored payment look new on the next request.
The Race Condition Most Teams Miss
Without a distributed lock:
- Request A arrives. Store check: not found.
- Request B arrives (duplicate, concurrent). Store check: not found.
- Request A processes payment. Stores result.
- Request B processes payment again. Overwrites result.
Customer charged twice. The lock closes this window.
The Atomic Write Problem
What if the server processes the payment but crashes before storing the idempotency key? The client retries. The server has no record of the first request. It processes again.
The cleanest solution is the transactional outbox pattern: write the payment result and the idempotency record in the same database transaction, then publish events asynchronously. It's more complex but eliminates this failure mode entirely.
The Takeaway
Retry is client resilience. Idempotency is server correctness. You need both, explicitly connected via the idempotency key passed on every retry.
When reviewing payment API designs, I check three things: Is the client sending the same key on retries? Is the server storing results durably with a lock? Is the store-and-execute step atomic? If any are missing, the system has a correctness gap that will manifest as duplicate charges under load.
If you found this useful, I run 1:1 mentoring sessions for Java/backend engineers at topmate.io/aliasgar_kantawala
My Java interview guides and system design resources are at aliasgarmk.gumroad.com
Top comments (0)