•CASE STUDY

Preventing Duplicate Request Processing (Idempotency Keys)

4 min read·708 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
  • What "exactly once" really means

Problem RestatementProblem

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.

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.

Deep Dive — Making the second request a no-opDeep dive

The user double-taps Pay, or the client retries after a timeout on a request that actually succeeded. The second request must produce the same outcome, not a second charge.

Weak

Check whether it already exists, then create

Look for a matching payment; if none, create one.

Sequence diagram
%%{init: {"look":"handDrawn","handDrawnSeed":7,"theme":"base","fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","themeVariables":{"fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","fontSize":"16px","primaryColor":"#fff4e6","primaryBorderColor":"#1e1e1e","primaryTextColor":"#1e1e1e","secondaryColor":"#e7f5ff","tertiaryColor":"#ebfbee","lineColor":"#1e1e1e","textColor":"#1e1e1e","mainBkg":"#fff4e6","nodeBorder":"#1e1e1e","clusterBkg":"#f8f9fa","edgeLabelBackground":"#ffffff","classText":"#1e1e1e"}}}%%
sequenceDiagram
  participant C1 as Request 1
  participant S as Server
  participant C2 as Request 2 - retry
  C1->>S: does a payment for this key exist?
  S-->>C1: no
  C2->>S: does a payment for this key exist?
  S-->>C2: no
  C1->>S: create payment
  C2->>S: create payment
  Note over S: two charges - the check and the create are separate

Check-then-act again. The duplicate arrives precisely when the first request is still in flight — that is what a timeout retry is — so the window the design ignores is the window it will always land in.

Good

Record the key after completing the work

Do the work, then store the idempotency key with the result, and return the stored result if the key is seen again.

This handles a retry that arrives after the first finished. It does nothing for the concurrent case, which is the common one: both requests are doing the work before either has written its key. The write also has to be in the same transaction as the work, or a crash between them loses the record and the next retry charges again.

Best

Claim the key first, then do the work

Insert the key as in_progress before touching anything, and let the primary key be the concurrency control:

CREATE TABLE idempotency_keys (
  scope TEXT,                  -- user or merchant, so keys cannot 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)
);
Architecture diagram
%%{init: {"look":"handDrawn","handDrawnSeed":7,"theme":"base","fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","themeVariables":{"fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","fontSize":"16px","primaryColor":"#fff4e6","primaryBorderColor":"#1e1e1e","primaryTextColor":"#1e1e1e","secondaryColor":"#e7f5ff","tertiaryColor":"#ebfbee","lineColor":"#1e1e1e","textColor":"#1e1e1e","mainBkg":"#fff4e6","nodeBorder":"#1e1e1e","clusterBkg":"#f8f9fa","edgeLabelBackground":"#ffffff","classText":"#1e1e1e"}}}%%
flowchart LR
  R["Request + Idempotency-Key"] --> INS{"Insert as in_progress"}
  INS -->|"inserted"| WORK["Do the work in a transaction"]
  WORK --> SAVE["Save response, status completed"]
  SAVE --> OUT["Return the response"]
  INS -->|"exists, completed"| REPLAY["Return the stored response"]
  INS -->|"exists, in_progress"| WAIT["409 or retry-after - the first one is still running"]
  INS -->|"exists, different request_hash"| ERR["422 - key reused with a different body"]
  • The insert is the lock. Exactly one request wins the primary key; the loser learns immediately that another is in flight, so the concurrent case is handled by the database rather than by timing.
  • Store the response, not just the fact. A replay must return the same status code and body, or the client sees a success it cannot correlate.
  • Hash the request body. The same key with a different body is a client bug — usually a key reused across payments — and answering it with the first payment's response would be worse than an error.
  • Scope keys per customer so two tenants cannot collide, and give in_progress rows a locked_until so a crashed request does not block its own retries forever.

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.

Wrap-UpWrap-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

Frequently Asked Questions

What is the Preventing Duplicate Request Processing (Idempotency Keys) system design question?

Preventing Duplicate Request Processing (Idempotency Keys) is a system design interview question asked at FAANG companies. It covers api design, distributed systems, payments and tests your ability to design scalable, production-ready systems. InterviewSkool's breakdown walks you through requirements, API design, architecture, and trade-offs.

Which companies ask the Preventing Duplicate Request Processing (Idempotency Keys) question?

OpenAI have reportedly asked variations of this question in system design interviews. The exact wording may differ, but the core design challenges remain the same.

How should I prepare for the Preventing Duplicate Request Processing (Idempotency Keys) interview question?

Start with the problem statement and scale estimates, then design the high-level architecture. Focus on the core components, data model, and API design. InterviewSkool's breakdown covers the full solution with mermaid diagrams and trade-off analysis to help you prep efficiently.

What level is the Preventing Duplicate Request Processing (Idempotency Keys) question?

This question is suitable for SDE-2, SDE-3, and Staff engineer interviews. The level guidance on this page provides specific tips for each level — SDE-2 candidates should focus on core architecture, while Staff engineers should discuss trade-offs, monitoring, and incremental rollouts.

Practice with a Mock Interview

Apply what you learned in a live system design mock interview with InterviewSkool's AI interviewer.

Start System Design Interview →