Command Palette

Search for a command to run...

Hectal
PHASE 12Advanced ~9 min· topic 4 of 6

Topic 12.4

Idempotency Keys: Stopping Double Payments

In one line

Clients send an Idempotency-Key with each operation; the server records the key before processing and stores the response, so retries and double clicks return the original result instead of repeating the operation. Redis makes the fast path cheap; the database makes it correct.

0/6 · 0%

Think of it like this

A cloakroom ticket. If you hand in your coat twice with the same ticket number, the attendant says "already have it" and gives you the same ticket back, instead of taking a second coat.

Key ideas

  1. 01

    Flow: the client generates a unique key per logical operation (UUID) and resends the same key on retries. The server tries SET idem:{key} PROCESSING NX EX 86400. If it succeeds, this request owns the operation; process it, then store the response (SET idem:{key} <response JSON> EX 86400). If NX fails, another request has the key: if the stored value is a response, return it; if it's still PROCESSING, return 409 or wait briefly.

  2. 02

    Why not GET then process: two requests 20 ms apart can both see "missing" and both process. SET NX makes the claim atomic.

  3. 03

    Bind the key to the request: store a hash of the request body with the key; a retry with the same key but a different body is a client bug, so reject it (422).

  4. 04

    Failure cases: the server crashes after charging but before storing the response, leaving PROCESSING until TTL. A retry then waits or gets 409; the payment provider's own idempotency key (pass yours through) prevents a second charge. When Redis is lost (failover without the key), a retry could reprocess, so the database needs its own guarantee: a unique constraint on the idempotency key in the payments table.

  5. 05

    Exactly-once is an illusion across networks: what you build is at-least-once delivery plus idempotent processing, which is effectively-once.

Code & diagrams

idempotency.mermaiddiagram
Rendering diagram…
IdempotencyFilter.javajava
public ResponseEntity<PaymentResponse> pay(String key, PaymentRequest req) {
    String redisKey = "idem:pay:" + key;
    String fingerprint = sha256(req);
    Boolean claimed = redis.opsForValue()
        .setIfAbsent(redisKey, "PROCESSING:" + fingerprint, Duration.ofHours(24));

    if (!Boolean.TRUE.equals(claimed)) {
        String v = redis.opsForValue().get(redisKey);
        if (v == null) return ResponseEntity.status(409).build();          // expired mid-flight
        if (v.startsWith("PROCESSING:")) return ResponseEntity.status(409).build();
        StoredResponse s = json.read(v, StoredResponse.class);
        if (!s.fingerprint().equals(fingerprint)) return ResponseEntity.unprocessableEntity().build();
        return ResponseEntity.status(s.status()).body(s.body());         // replay original result
    }

    // The DB has a UNIQUE(idempotency_key) constraint: the real correctness guarantee.
    PaymentResponse result = payments.charge(req, key);                    // passes key to the provider too
    redis.opsForValue().set(redisKey, json.write(new StoredResponse(200, result, fingerprint)),
                            Duration.ofHours(24));
    return ResponseEntity.ok(result);
}

Interview problem

The problem

PAY, PAY within 20 ms

A user double-clicks PAY and two requests with the same idempotency key arrive 20 ms apart. Design the protection. Follow-up: what if Redis crashes in the middle?

You're given

  • Payments API
  • Clients send Idempotency-Key
  • Must never charge twice

The interviewer follows up

01

Why not rely on Redis alone?

When it breaks

Idempotency check done with GET-then-process

What you see

Two concurrent requests both see no key and both charge the card.

Fix & prevent

Claim with SET NX atomically, and back it with a database unique constraint.

Explain it without notes

01

Why is exactly-once processing an illusion, and what do you build instead?

Practice

01

Build idempotency middleware for one POST endpoint and test concurrent duplicates with 10 parallel requests using the same key.

Trade-offs

  • ↔

    Longer idempotency TTLs protect late retries but use more memory; 24 hours is common for APIs.

Done when you can

  • I can implement idempotency keys with an atomic claim and stored responses.

  • I back Redis-based idempotency with database constraints for money.