Home/Learn/Microservices/Idempotency in APIs

Idempotency in APIs

Intermediate
Patterns

An 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.

Java — Spring MVC interceptor
@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.

SQL — atomic deduplication table
-- 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.

Java — WebClient retry with stable key
// 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 → safe

Key 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 Aria
1

Why is POST not idempotent by default, and how do you make it idempotent?

EasyStripe
2

How would you prevent double-charging in a payment service that retries on timeout?

MediumPayPal
3

Describe a race condition in idempotency key handling and how to prevent it.

HardAmazon
4

What TTL would you set on idempotency keys and how does it affect client retry windows?

MediumRazorpay
5

How does idempotency interact with database transactions — can a key check and business logic share a transaction?

HardUber

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.

Loading discussion…