SD System Design In Depth
0%
case system-designcasestripeidempotency

Case: Stripe's idempotency keys

Module 6.4: Real-World Architecture Evolution & Incidents Updated Sep 18, 2026
📄 Cheat Sheet
Designing Idempotent API Endpoints for Payments at Stripe
Arpit Bhayani Designing Idempotent API Endpoints for Payments at Stripe Watch on YouTube ↗

Arpit Bhayani walks through Stripe's published idempotency design, the key lifecycle, concurrency and failure handling, rather than a generic payment-system interview.

0:00 / --:--

Case: Stripe's Idempotency Keys

An idempotency key tells a server which intended operation a request belongs to. That matters deeply in payment systems: when the server completes work but its reply never reaches the caller (due to a network timeout), another attempt should recover the result without charging the customer twice.

Stripe's implementation of idempotency is the industry gold standard for building robust distributed systems where network failures are inevitable.

The Problem: The Lost Response

Imagine a client sends a POST /v1/charges request to Stripe to charge $50. Stripe processes the payment, deducts the money, and sends back an HTTP 200 OK. But right before the response reaches the client, the client's wifi drops.

The client's code receives a timeout. The client doesn't know if the request never reached Stripe, or if Stripe processed it but the response was lost. If the client retries the request, the user might be charged another $50. If the client doesn't retry, the user might not get their goods, even though they were charged.

The Solution: Idempotency Keys

Stripe supports client-supplied Idempotency-Key headers for eligible mutating requests. One key names one intended operation, so a retry must reuse both the key and the same parameters; a new checkout attempt needs a new key.

POST /v1/charges
Idempotency-Key: a1b2c3d4-e5f6
Amount: 5000

When Stripe receives a request, it checks its database (or a fast distributed cache) to see if it has seen this key before.

  1. First Request: The service validates and begins one operation under the key. It retains the resulting status and response according to the provider's documented idempotency policy.
  2. Network Fails: The client does not get the response and retries with the same key and parameters.
  3. Second Request: If the first operation has a retained result, the service returns it instead of starting another charge. A concurrent in-progress request or changed parameters must be handled explicitly rather than treated as an ordinary replay.

Where to Store the Keys

An API can atomically claim an idempotency key with its local operation record and reject the same key with changed parameters. It cannot place an external card-network effect in that database transaction. After a timeout, the provider may have accepted the charge even though the local response record is incomplete, so recovery needs the same downstream operation identity, an explicit unknown state, provider lookup or webhook evidence, and reconciliation.

sequenceDiagram participant Client participant StripeAPI participant DB Client->>StripeAPI: POST /charges (Key: X) StripeAPI->>DB: Check Key X DB-->>StripeAPI: Not Found StripeAPI->>DB: Process Payment & Save Key X + Response StripeAPI--xClient: (Network Drop) HTTP 200 Client->>StripeAPI: Retry POST /charges (Key: X) StripeAPI->>DB: Check Key X DB-->>StripeAPI: Found! Return saved response StripeAPI-->>Client: HTTP 200 (Recovered)

Stripe may remove keys once they are at least 24 hours old to save storage space, as retries usually happen within minutes, not days.

Worked example: a charge with a lost response

  1. Client generates key k-7f3a (a V4 UUID) for "charge order 991 for 50.00" and stores it with the order.
  2. Request 1 arrives. The server inserts (key=k-7f3a, request_hash=h, state=started) in the same database transaction that creates the local charge record. The unique constraint on the key makes this claim atomic.
  3. The server calls the card processor, passing its own stable reference derived from the charge id. The processor approves. The server stores the response body and sets state=finished.
  4. The client's connection drops before the response arrives.
  5. Request 2, same key and same parameters: the server finds state=finished and replays the stored response. No second authorization.

Every case the server must handle

Situation on arrivalCorrect response
Key never seenClaim it atomically and execute
Key finished, same parametersReplay the stored status and body, even if it was an error
Key finished, different parametersReject: the key is being misused
Key in progress (concurrent request)409 Conflict; client retries later
Key started, then server crashed mid-wayResume from the last recorded recovery point, or query the processor using the stable reference, then finish
Key older than retention windowTreated as new; Stripe documents removal after at least 24 hours

Designing the recovery points

Brandur Leach's widely cited write-up of Stripe-like keys splits a request into atomic phases separated by recovery points (for example started, charge_created, finished). Each local phase commits in one database transaction together with the new recovery point. Foreign calls happen between phases and must themselves be idempotent, so re-executing from a recovery point is safe.

Client-side rules

  • Retry only on network errors, timeouts and 5xx or 409 responses; use exponential backoff with jitter.
  • Never reuse a key for a genuinely new attempt, such as a second checkout.
  • Persist the key before sending, so an app restart still retries with the same key.
Check yourself Question 1 of 3
Replaying an error apply
From: Every case the server must handle

A charge request with key k-12 finished with a 402 card_declined response. The client retries with the same key and the same parameters. What should the server return?

Key Takeaways

Network timeouts are ambiguous: you don't know if the request failed or the response failed.
APIs that mutate state (payments, creates, deletes) must be idempotent to allow safe retries.
Clients generate a unique UUID and pass it in the Idempotency-Key header.
The server caches the resulting HTTP response against the idempotency key and replays it on retries.
Claim the key atomically with local mutation state; external effects still require provider idempotency, unknown-outcome recovery, and reconciliation.
Was this lesson helpful? Spot an issue or have a suggestion? Let us know on GitHub.
Report an issue