Idempotency in APIs
IntermediateAn idempotent operation produces the same result when called multiple times; implement via idempotency keys stored in Redis or a DB to deduplicate retries.
Overview
In distributed systems, networks are unreliable and clients retry failed requests. Without idempotency, a payment service could charge a customer twice if the response is lost in transit. Idempotency keys — client-generated UUIDs sent in a header like Idempotency-Key — let the server detect duplicates and return the cached result of the first successful execution. The pattern is essential for any mutating operation (POST, PATCH, DELETE) that must be safe to retry. Storage choices range from Redis (fast, TTL-based expiry) to a DB table (durable, auditable). The implementation must be atomic: check-and-set in a single Redis transaction or an INSERT ... ON CONFLICT DO NOTHING in Postgres to prevent race conditions between two simultaneous identical requests.
Redis-based idempotency filter
A Spring HandlerInterceptor checks the Idempotency-Key header before the controller runs. If a cached response exists it is replayed immediately. After the controller succeeds, the response body is stored in Redis with a 24-hour TTL.
@Component
public class IdempotencyInterceptor implements HandlerInterceptor {
private final StringRedisTemplate redis;
@Override
public boolean preHandle(HttpServletRequest req,
HttpServletResponse res, Object handler) throws Exception {
String key = req.getHeader("Idempotency-Key");
if (key == null) return true; // key optional for GET
String cached = redis.opsForValue().get("idem:" + key);
if (cached != null) {
res.setStatus(200);
res.setContentType("application/json");
res.getWriter().write(cached);
return false; // short-circuit, reply cached
}
return true;
}
// Call this from controller advice after response committed
public void cacheResponse(String key, String responseBody) {
redis.opsForValue().set("idem:" + key, responseBody,
Duration.ofHours(24));
}
}DB-level deduplication (Postgres / MySQL)
For payment or order services where durability is critical, persist idempotency keys in a table. Use INSERT ... ON CONFLICT to make the check-and-insert atomic even under concurrent requests.
-- schema
CREATE TABLE idempotency_keys (
key VARCHAR(100) PRIMARY KEY,
status VARCHAR(20) NOT NULL DEFAULT 'processing',
response JSON,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
-- on request arrival (Java + JDBC or JPA native query)
INSERT INTO idempotency_keys (key) VALUES (?)
ON CONFLICT (key) DO NOTHING;
-- after insert: check rows-affected
-- rows == 1 -> first request, proceed
-- rows == 0 -> duplicate, SELECT response WHERE key = ?
-- on success
UPDATE idempotency_keys
SET status = 'success', response = ?::json
WHERE key = ?;Client-side retry with idempotency key
Clients must generate a stable key per logical operation (not per HTTP call) and resend it on every retry. Using Spring's RestTemplate or WebClient with a fixed key ensures the server deduplicates correctly.
// Generate once per operation, persist across retries
String idempotencyKey = UUID.randomUUID().toString();
WebClient client = WebClient.create("https://payments.internal");
Mono<PaymentResponse> response = client.post()
.uri("/payments")
.header("Idempotency-Key", idempotencyKey)
.bodyValue(paymentRequest)
.retrieve()
.bodyToMono(PaymentResponse.class)
.retryWhen(Retry.backoff(3, Duration.ofMillis(500))
.filter(ex -> ex instanceof WebClientRequestException));
// retries will carry the same idempotencyKey → safeKey Points to Remember
- 1HTTP GET and PUT are naturally idempotent; POST, PATCH, and DELETE need explicit deduplication logic.
- 2Idempotency keys must be client-generated (UUID v4) and scoped to a single logical operation, not per-retry.
- 3Redis SET NX with TTL is the fastest implementation; a DB table is better when you need audit history.
- 4The check-and-store must be atomic — race conditions between two identical concurrent requests can cause double-processing.
- 5Store the full response body, not just a "done" flag, so replays return identical HTTP status and payload.
- 6Set a reasonable TTL (24h–7d) to bound storage growth; communicate the window to API consumers.
Interview Questions
Sign in to ask AriaWhy is POST not idempotent by default, and how do you make it idempotent?
How would you prevent double-charging in a payment service that retries on timeout?
Describe a race condition in idempotency key handling and how to prevent it.
What TTL would you set on idempotency keys and how does it affect client retry windows?
How does idempotency interact with database transactions — can a key check and business logic share a transaction?
Ask Aria about Idempotency in APIs
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.