From the phone, these look identical
- Never arrived: a retry is safe
- Failed halfway: it depends what committed
- Committed, reply lost: a retry repeats it
Idempotent means the same response?
Bad: Same response
DELETE answers 204, then 404. Both are fine.
Good: Same effect
Doing it n times leaves the state of doing it once.
Climb only as high as the write needs
Set instead of increment. Check a version. Let the client pick the ID. For payments, an idempotency key with the stored result.
Claim the key and save the reply in one transaction
A unique index on (account, key) lets one attempt win. The payment and its stored response commit together, so a retry replays the answer.
A client wrapper mints a fresh key per attempt. Safe?
The bug
No. Each retry becomes a new payment. Mint the key on the first tap, save it on the device, and reuse it.
Every hop dedupes its own retries
Each service derives downstream keys from the key it received, its caller and the step. A retried checkout recreates the same keys, so every hop spots the duplicate.
Where it shows up
- Payments: idempotency keys at the API
- Order creation: client-chosen IDs
- Ledgers: version-checked updates
- Consumers: an inbox of seen message IDs
Say it like this
A timeout doesn’t say whether we committed. Make updates naturally idempotent; for payments, the client sends a key once per operation, and we store it with the result in one transaction.