pattern

Idempotency

also called Idempotency Key, Exactly-Once Effect

The property that performing an operation twice has the same effect as performing it once — the only practical defence against the duplicates a retrying client will inevitably send.

idempotencyretriespaymentsstripecorrectness

Definition

An operation is idempotent when applying it more than once produces the same result as applying it once. DELETE /orders/42 is naturally idempotent. POST /charges is not, and that is where the money is lost.

Why it matters

A client that sends a request and receives no response cannot tell whether the request never arrived or whether it succeeded and the response was lost. Those two states are indistinguishable from outside, and the only reasonable client behaviour is to retry. Therefore your server will receive duplicates. Not might — will. Idempotency is not a defensive nicety; it is the acceptance of a fact about networks.

The related trap is the phrase "exactly-once delivery". Across a network boundary it is not available. What is available is at-least-once delivery plus idempotent processing, which produces an exactly-once effect. Any design that depends on exactly-once delivery has a correctness bug that has not fired yet.

Implementation patterns

Client-supplied idempotency key. The client generates a unique key per logical operation and sends it as a header. The server stores the key with the request fingerprint and the response. This is the pattern Stripe made standard, and the details matter:

  1. Insert the key into a uniquely-indexed table before doing any work. The unique constraint is the concurrency control; two simultaneous requests race and exactly one wins.
  2. The loser of the race must not proceed. It either returns the stored response or, if the first request is still in flight, returns a "request in progress" status so the client retries later.
  3. Store a fingerprint of the request body alongside the key. If the same key arrives with different parameters, that is a client bug — return an error rather than silently returning the first result.
  4. Persist the response, not just the fact of completion, so the replay is answered identically.
  5. Expire keys after a defined window — typically 24 hours — and say so in the documentation, so a client retrying a week later gets a defined behaviour rather than a duplicate charge.

Natural idempotency. Design the operation so repetition is harmless: SET balance = 100 rather than ADD 50, or an upsert keyed on a business identifier. Cheapest option when available.

Conditional writes. Compare-and-set on a version or ETag. The second attempt fails the precondition and is a no-op.

Deduplication at the consumer. For events and messages, keep a store of processed message IDs with a retention window. Necessary because brokers deliver at least once by design.

Failure scenarios

  • Dedupe after the side effect. The charge is made, then the process crashes before recording the key. The retry charges again. The record must be durable before the effect, not after.
  • Idempotency in memory. A per-instance cache of seen keys works right up to the moment there are two instances, which is always.
  • Keys scoped wrongly. Scoped globally, one tenant can collide with another. Scoped per request rather than per logical operation, the retry generates a new key and defeats the point.
  • Non-idempotent retries in the middle tier. The API is idempotent; the internal call from the API to the ledger is not, and the internal client retries.
  • Idempotent create, non-idempotent side effects. The order is created once, but the notification email fires on every attempt.

Trade-offs

The idempotency store is a write on the hot path and a piece of state that must be as available as the operation it protects. Retention costs storage and expiry creates a cliff. Strict fingerprint checking rejects some legitimate clients that regenerate payloads. And for high-volume, low-value operations — page views, log lines — the cost is not worth paying at all: let them duplicate.

Interview question

"Design an idempotent POST /payments. Where exactly do you write the idempotency record relative to calling the payment processor, and what happens if the process dies between the two?"