Idempotency

BackendTier 2 · build and shipPhase 4 · Take money

Designing an action so that doing it twice has the same effect as doing it once. Essential where retries and double-clicks happen.

Networks are unreliable, so a client may send the same request twice, on a retry or a double click, and for an action like charging a card, doing it twice is a real loss. You cannot always tell a retry from a genuine second request. Idempotency exists so repeating the same request has the same effect as sending it once, which makes retries safe.

Networks drop and retry. Users double-click. Without protection, a retried "charge card" could charge twice. Idempotency prevents this: each attempt carries a unique key, and the server, seeing a key it already processed, returns the original result instead of doing it again.

So the customer clicking Pay three times, or the network retrying, all resolve to exactly one charge and one receipt. The operation is safe to repeat.

A junior PM, stuck

Finance flagged that a customer got charged twice for one order on Friday, and the backend dev says "checkout needs idempotency keys." Now I have to spec that, and I do not actually know what a key does or how it stops a second charge. I do not want to write a vague ticket that the dev has to rewrite.

The double charge happened because the network retried the order POST and the server treated the retry as a brand new order. An idempotency key is the fix, and it is simpler than it sounds: the client stamps one unique key on the intent to buy, and the server refuses to run the same key twice. I captured how the same call behaves with a key in place, the original send and the retry three seconds later. The two responses tell the whole story.

POST /api/orders twice with the same Idempotency-Key
13:07:12 first send
1.1
: 73921
{ "product_id": "p_1042", "qty": 1 }
1.1 201
{ "order_id": "o_5512", "amount": 649, "status": "charged" }
13:07:15 network retry, same key
1.1
: 73921
{ "product_id": "p_1042", "qty": 1 }
1.1 200
{ "order_id": "o_5512", "amount": 649, "status": "charged", "replayed": true }

Click a step to see the lines it points at.

Speccing "stop double charges" without the key. The dev cannot dedupe what it cannot identify. The unique key per intent is the concrete thing that makes a retry recognizable; leave it out and the ticket is just a wish.
Assuming the frontend disabling the Pay button is enough. That helps with double-clicks but does nothing for a network retry, which fires with no user involved. The safety has to live on the server.
Thinking a key makes every retry return a fresh order. It returns the same order. If your flow needs a genuine second order, that is a different key and a different intent, not a bug in idempotency.
Reusing one key across different orders. The key must be unique per intended action. Share a key by accident and the second real order gets swallowed as a duplicate, which is the opposite failure.
Confusing this with the race-condition fix. Idempotency handles the same request arriving twice; a race is two different requests overlapping. Related, but you spec them separately.

Write the ticket as: "Checkout must send one Idempotency-Key per order attempt. If the server sees a key it already processed, return the original order and do not charge again." You named the key, the retry it defends against, and the exact server behavior. That is idempotency specified, and it is what would have saved Friday's customer from two charges.

Any action that moves money or creates records needs this. "What if they double-click Pay?" should already be answered; if not, raise it.
"We got charged twice" incidents are idempotency failures; understanding the fix lets you spec it into payment and order flows from the start.

"Add an idempotency key to checkout so retries don't double-charge."

Appears in Phase 4, Take money.