CASE STUDY

Preventing Duplicate Request Processing (Idempotency Keys)

3 min read·549 words·Beginner

Asked at

1 candidate report in Apr 2026

How to use this case study

SDE-2 / Mid

Explain why duplicates happen (double click, retries, network) and how a client-generated idempotency key plus a server-side store makes the operation happen once.

SDE-3 / Senior

Handle concurrent duplicates in flight (in-progress state and locking), storing and replaying the original response, key expiry and request fingerprint checks.

Staff / Principal

Discuss idempotency across multiple services and side effects (outbox, downstream keys), multi-region stores, and what "exactly once" really means.


0) Problem Restatement

A client may send the same request twice: the user double-clicks "Pay", the app retries after a timeout (the first request actually succeeded), or the network duplicates a packet. For operations like charging a card or creating an order, doing it twice is a serious bug. Design a reliable way (asked at OpenAI) to make the operation take effect exactly once and give the client a consistent response every time it retries.


1) The Core Idea: Idempotency Keys

  • The client generates a unique key (a UUID) per logical operation (not per HTTP attempt) and sends it: Idempotency-Key: 5f3c.... All retries of the same operation reuse the same key.
  • The server remembers keys it has processed and their results. If the key was seen before, it returns the saved response instead of doing the work again.

"Idempotent" means doing it once or many times has the same effect.


2) Server Design

CREATE TABLE idempotency_keys (
  scope TEXT,                  -- e.g. user or merchant id, so keys can't collide across customers
  key TEXT,
  request_hash TEXT,           -- fingerprint of the request body
  status TEXT,                 -- in_progress, completed
  response_code INT, response_body JSONB,
  locked_until TIMESTAMP, created_at TIMESTAMP,
  PRIMARY KEY (scope, key)
);

2.1 Flow

Architecture Diagram

flowchart LR
    R["Request + Idempotency-Key"] --> I{"Insert key as in_progress"}
    I -->|"inserted - new"| W["Do the work in a transaction"]
    W --> S["Save response, status completed"]
    S --> OUT["Return response"]
    I -->|"exists, completed"| REPLAY["Return saved response"]
    I -->|"exists, in_progress"| WAIT["409 / retry later"]
    I -->|"exists, different body"| ERR["422 key reused with different request"]
  1. Try to insert (scope, key, request_hash, status='in_progress'). The primary key makes this atomic: only one request can win.
  2. Won → do the operation, ideally in the same database transaction as saving the result (status='completed', response). Then either both happen or neither.
  3. Lost, and the existing row is completed → return the stored response. The client sees the same result as the first time.
  4. Lost, and still in progress (a concurrent duplicate) → return 409 Conflict / "try again", or wait briefly and then return the result.
  5. Same key, different request body (hash mismatch) → reject with an error. It's a client bug.

2.2 Crashes in the middle

If the server crashes after inserting in_progress but before finishing, the row has a locked_until. After it expires, a retry may take over. For external side effects (a charge at a payment provider), the work itself must also be safe to repeat: pass the same idempotency key downstream to the provider, so a second attempt returns the original charge instead of creating a new one.


3) Other Details

  • Expiry: keep keys for 24 hours to 7 days (clients shouldn't retry after that), then clean them up with a TTL.
  • Scope: keys are unique per account, so two customers can't collide.
  • Where to store: the same database as the business data is best (one transaction). Redis works for low-risk cases (SET key NX + a stored response), but loses the atomicity with DB writes.
  • Natural idempotency: some operations are idempotent by design. PUT /users/42 {name} sets a value, and "create order with client-provided order_id" uses a unique constraint. Prefer these when possible.
  • Messaging: for events and queues, the same idea applies with the event ID. Consumers keep processed IDs, or make writes upserts.


4) Wrap-Up

Have clients send one idempotency key per logical operation, reused across retries. On the server, atomically insert the key as in-progress, do the work and store the response in the same transaction, replay the stored response for repeats, reject concurrent or mismatched duplicates, and expire old keys. Pass the key to downstream providers so external side effects are also deduplicated. That's what "exactly once" means in practice: at-least-once delivery plus idempotent processing.

More Case Studies

Practice with a Mock Interview

Apply what you learned in a live system design mock interview with our AI interviewer.

Start System Design Interview →