software-design/idempotency-why-clicking-pay-twice-should-charge-you-once.md
Idempotency: why clicking Pay twice should charge you once
You tap Pay, the screen shows an error, you tap again, and your card is charged twice. Networks drop messages, so a phone often cannot tell whether a request failed or only its reply was lost.
This article explains idempotency: an operation that has the same effect whether it runs once or five times. You will see why "add a charge" is not idempotent, how an idempotency key turns it into one, and then try it in a simulated shop, with and without a key, to watch the ledger.

You tap Pay. The screen freezes for a few seconds, then shows an error: "Something went wrong, please try again." You tap Pay again. A week later you notice your card was charged twice. Whose fault is that, and how can the people who built the shop make sure it never happens?
The problem: the reply can get lost
When your phone talks to a shop's server, it sends a request ("charge this card $20") and waits for a response ("done"). Either message can be lost on the way, because networks drop data. That gives your phone three possible outcomes, and it can only see two of them:
The request never arrived, so nothing was charged. The request arrived and the server charged you, but the response was lost, so you still got an error. From your phone's point of view these two look identical: no answer came back. The phone cannot tell whether it is safe to try again.
If the server simply obeys every request it receives, retrying in the second case charges you twice. The way out is a property called idempotency (say it: eye-dem-POH-ten-see).
What idempotent means
An operation is idempotent if doing it several times has the same effect as doing it once. Setting a number is idempotent: set the balance to 150 twice and it is still 150. Adding to a number is not: add 50 twice and you get 200.
let balance = 100;
const setBalance = (n) => { balance = n; }; // idempotent
const addToBalance = (n) => { balance += n; }; // not idempotent
setBalance(150); setBalance(150);
console.log("set twice:", balance); // set twice: 150
balance = 100;
addToBalance(50); addToBalance(50);
console.log("add twice:", balance); // add twice: 200
The web builds this idea into its vocabulary. The HTTP rules say that GET (read something), PUT (replace something with this exact value) and DELETE (remove something) are meant to be idempotent, so a program may safely repeat them after a failure. POST, the method most often used for "create a new thing", is not guaranteed to be. Deleting the same item twice is still idempotent even though the second reply may say "not found": the intended effect, the item being gone, is the same.
A payment is naturally a "add a charge" operation, so it behaves like addToBalance. We need to make it behave like setBalance without changing what it does.
The fix: give every attempt a name
The trick is for the client to attach a unique label, called an idempotency key, to each intention. Your phone makes up one random key when you first tap Pay, and sends the same key on every retry of that same payment. The server keeps a notebook of keys it has already handled, along with the answer it gave. When a request arrives with a key already in the notebook, it does not charge again. It just repeats the saved answer.
First, a server with no notebook. Two identical requests, two charges:
const charges = [];
function chargeNoKey(req) {
const result = { id: "ch_" + (charges.length + 1), amount: req.amount };
charges.push(result);
return result;
}
chargeNoKey({ amount: 20 }); // first try (the reply gets lost)
chargeNoKey({ amount: 20 }); // retry
console.log(charges.length, "charges");
console.log(charges);
Output:
2 charges
[ { id: 'ch_1', amount: 20 }, { id: 'ch_2', amount: 20 } ]
Now the same server with a notebook (a Map) of keys it has seen:
const charges = [];
const seen = new Map(); // key -> saved result
function charge(req) {
if (seen.has(req.key)) return seen.get(req.key); // already done: replay
const result = { id: "ch_" + (charges.length + 1), amount: req.amount };
charges.push(result);
seen.set(req.key, result);
return result;
}
const key = "order-8841-attempt-1";
console.log(charge({ key, amount: 20 }));
console.log(charge({ key, amount: 20 }));
console.log(charges.length, "charge");
Output:
{ id: 'ch_1', amount: 20 }
{ id: 'ch_1', amount: 20 }
1 charge
Both calls get the same answer, ch_1, and only one charge exists. Try the real thing yourself below.
Details that matter in real systems
The key belongs to the intention, not the tap. Make a new key when the user starts a new payment, and reuse it for every retry of that payment. If you generate a fresh key on every retry, the server sees a new request each time and you are back to double charges.
Keys do not live forever. A server cannot keep every key for ever. Stripe, a payments company whose API is a well-known example, documents that for its version 1 API it may remove keys once they are at least 24 hours old. After that, a repeated key counts as a new request. Retries are meant for the next seconds and minutes, not next week.
Same key, different request is a mistake. If a client reuses a key but changes the amount, the answer "replay the saved result" would be wrong. Stripe's API compares the new parameters with the original ones and returns an error if they differ. Our little Map above skips that check, so a real version would store the request alongside the result.
The notebook must be saved with the charge. If the server charges the card but crashes before writing the key down, a retry will charge again. Real systems write the key and the charge together, usually in one database transaction.
What this means for your own code
Whenever your code does something with a side effect (charge a card, send an email, create an order) and the call can fail in a way that leaves you unsure whether it happened, ask: "what happens if this runs twice?" If the answer is "something bad", either make the operation idempotent by design (prefer "set the status to shipped" over "add one to shipped count"), or give each attempt a key so repeats can be recognised. Safe retries are one of the quiet differences between a program that works on a good day and one that works on a bad day.