Interview concept

Idempotency and retries

How to make sure a retried request doesn't charge someone twice or do the same thing twice.

System design concept

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.

Clientsends a unique keyServerseen this key?Do the workonly the first timeSaved resultskey → answer
The second request with the same key gets the saved answer back. Nothing happens 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

ApproachGoodBad
Idempotency keysWorks for any operationYou have to store keys, usually for a day or so
Naturally idempotent operationsNothing extra to storeNot everything can be written that way
Checking state firstSimple, 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.

  1. 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.
  2. The server tries to insert the key into a table with a unique constraint, with a status of 'in progress'. The first request succeeds.
  3. It charges the card, then saves the response, '$50 charged, payment ID p_123', next to the key.
  4. 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.
  5. 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.
  6. If the retry has the same key but a different amount, that's a bug in the client. Return an error instead of guessing.
  7. 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.