What it is
Something is idempotent if doing it twice has the same effect as doing it once. That matters because requests get retried all the time: someone double-clicks, a phone loses signal, or a server times out and tries again. If 'pay $50' isn't idempotent, a retry charges twice.
How it works
The client makes up a unique ID, often called an idempotency key, and sends it with the request. Every retry sends the same key. The server saves the key with the result the first time. If the key shows up again, it returns the saved result instead of doing the work again.
The tricky part is two copies of the request arriving at the same moment. Save the key first, in a way that only one of them can succeed, like a unique column in a database. The loser waits for the winner's result.
Some things are naturally idempotent. 'Set the balance to $50' is safe to repeat. 'Add $50 to the balance' isn't. Where you can, design operations like the first kind.
Trade-offs
| Approach | Good | Bad |
|---|---|---|
| Idempotency keys | Works for any operation | You have to store keys, usually for a day or so |
| Naturally idempotent operations | Nothing extra to store | Not everything can be written that way |
| Checking state first | Simple, like 'is this order already paid?' | Two requests at once can both pass the check |
In an interview
- Bring it up any time money moves or a message could be delivered twice.
- Say who makes the key: usually the client, before the first attempt.
- Explain what happens when two copies of a request arrive at the same time.
Worked example
A retried payment
A phone app sends 'charge $50 for order 789'. The server charges the card, but the response is lost when the phone switches from Wi-Fi to mobile data. The app retries.
- Before the first attempt, the app generates a random key, like 4f1c..., and stores it with the pending order. Every retry sends the same key.
- The server tries to insert the key into a table with a unique constraint, with a status of 'in progress'. The first request succeeds.
- It charges the card, then saves the response, '$50 charged, payment ID p_123', next to the key.
- The retry arrives. Inserting the key fails because it already exists. The server reads the saved response and returns it. The card isn't charged again.
- If the retry arrives while the first request is still running, the key's status is 'in progress'. The server tells the client to try again shortly instead of starting a second charge.
- If the retry has the same key but a different amount, that's a bug in the client. Return an error instead of guessing.
- Keys are deleted after a day or so. Retries don't happen days later, and the table would grow forever otherwise.
Follow-up questions
Things interviewers tend to ask once you've brought this up.
- What if the server crashes after charging but before saving the response?
- That's the hardest case. Pass the same key to the payment provider too. Most of them accept idempotency keys, so asking again returns the original charge instead of a new one.
- Which HTTP methods are idempotent?
- GET, PUT and DELETE are supposed to be. POST isn't, which is why POST endpoints that matter need idempotency keys.
- How is this different from deduplicating messages from a queue?
- Same idea, different source of the ID. For a queue you use the message's ID, or a business ID like the order ID, and skip ones you've already processed.
- Why not have the server generate the key?
- The whole point is recognizing a retry. If the server makes a new key for each request, it can't tell that two requests are the same attempt.
Common mistakes
- Checking 'did I already do this?' and then doing it, in two separate steps, so two simultaneous requests both pass.
- Generating a new key on each retry.
- Saving the key only after the work succeeds, so a crash in the middle loses it.
- Forgetting to pass idempotency through to downstream calls like the payment provider.
- Keeping keys forever.