One coffee. Two charges. Why retries need idempotency
Published
A coffee payment succeeds, but its reply is lost. Retrying with a new payment key can charge you again. Design for failure means deciding how the system recovers before things break.
Explanation & code
This is a local SQLite payment simulation. The server must atomically store the key and payment, handle concurrent requests and reject changed parameters. Real payment providers must support idempotency too; follow their retention and retry rules.
Understand it. Then fix it.
No reply does not mean no payment
The server commits the $5 payment. Its reply is lost. The app sees a failed request but does not know whether the payment happened. Treating a retry as a new purchase can charge another $5.
Keep one key for one purchase
Persist a payment key with the order. Our custom retry handler sends the same order and key each time. A new purchase needs a new key. This is an illustrative client excerpt, not a payment SDK call.
const payment = {
orderId: 'coffee-42',
key: 'pay-42'
};
const retry = () => pay(payment);
// Same stored key on every try.The server makes retries safe
Idempotency means repeating a request without repeating its effect. Our local example uses a transaction and a unique account/key pair: an existing matching request returns its stored receipt; a new one records the payment and key together. Concurrent requests cannot insert two charges. A header alone does not provide this protection.
Test the failure again
Lose the reply after committing the payment, then retry with the same key: the stored receipt returns and the total stays $5. The companion also tests a new key, changed parameters, restarts and concurrent connections. A local transaction cannot undo an external provider charge. Use provider-supported keys and reconcile unknown status when a safe retry is unsupported.
Code blocks are teaching excerpts. Keep the surrounding error handling and application requirements.
Save the code excerpts ↓Read the full transcript
The app said my coffee payment failed. I hit retry. Why did I pay twice? The server took five dollars. Then its reply got lost. No reply does not mean no payment. So hiding the retry button fixes it? No. Network retries can still happen. Design for failure means planning what happens when things break. Make repeating this payment safe. Give this order one payment key. Keep that key when you retry. A new key can create another charge. This is idempotency: repeating a request without repeating its effect. The server records the key and payment together, even when requests arrive at once. Same key and same order: return the saved result. Real payment providers must support the key too. Now lose the reply again. Retry: same key, same receipt. Total: five dollars. I ordered a coffee. Not the whole coffee shop.