Idempotency: The One Idea That Saves Payment Systems
Networks retry, and retries double-charge customers unless your system is idempotent. Here is how idempotency keys turn unsafe retries into safe ones.
A customer taps Pay. The request reaches your server, the charge succeeds, and then the response times out on the way back. The client retries. Without one specific safeguard, you just charged them twice.
That safeguard is idempotency, and it is the difference between a payment system you can trust and one that generates support tickets and chargebacks.
What idempotent means
An operation is idempotent if doing it twice has the same effect as doing it once. Reading a row is naturally idempotent. Setting a balance to 50 dollars is idempotent. But incrementing a balance by 50 is not, and neither is creating a charge. Run it twice, get charged twice.
The whole job is turning unsafe, effectful operations into safe-to-retry ones.
Why retries are not optional
In a distributed system you cannot tell the difference between these two cases:
- The request never arrived.
- The request succeeded but the response was lost.
From the client they look identical: a timeout. So clients must retry, and that means your server will receive duplicate requests. This is not an edge case. At scale it happens constantly.
The idempotency key
The fix is an idempotency key: a unique token the client generates and attaches to a request. The server uses it to recognize and deduplicate retries.
The flow:
- The client generates a unique key (a UUID) for the logical operation and reuses it across retries.
- The server checks if it has seen that key.
- If not, it performs the work and stores the result keyed by that token.
- If yes, it returns the stored result without re-executing.
Stripe, PayPal, and every serious payment API work exactly this way.
Getting the details right
The concept is simple. The correctness lives in the details:
- Store the key and the result atomically with the charge. If you charge first and record the key second, a crash between them reintroduces the double charge. Use a single transaction, or a unique constraint on the key column so a concurrent duplicate fails loudly.
- Handle in-flight duplicates. Two retries can arrive simultaneously. A unique constraint or a lock on the key ensures only one wins and the other waits for the stored result.
- Scope and expire keys. Keys belong to a user and an operation. Keep them long enough to outlive any retry window, then expire them so the table does not grow forever.
- Return the same response. A retry should get the original result, not a fresh error.
Beyond payments
Idempotency is everywhere once you see it: webhook delivery, message queue consumers, order creation, account signup. Any time an at-least-once delivery guarantee meets an effectful action, you need it. It pairs naturally with rate limiting, since both are about controlling how requests hit your system under pressure.
These patterns are the bread and butter of system design, and you can drill the broader distributed-systems fundamentals until they are reflexive. The roadmaps lay out a sensible order.
Want to know if your instinct for safe retries holds up under interview pressure? Face an AI judge matched to your tier at Cruxible and see whether you can still beat the machine.