Idempotency
An idempotent operation produces the same result no matter how many times you call it. In distributed systems where retries are inevitable, this property is the difference between a robust API and one that double charges every customer.
The problem in one sentence
Networks lose packets. Clients time out and retry. Without idempotency, retries duplicate work. The customer gets charged twice. The email goes out twice. The order is shipped twice. Idempotency is how you make a retry safe.
Formally, an operation is idempotent if calling it once and calling it ten times have the same effect. Setting a value is idempotent. Incrementing a counter is not. Deleting a record is idempotent (it is gone, deleting it again is a no-op). Creating a record is usually not, because two creates make two records.
How HTTP methods line up
| Method | Idempotent? | Why |
|---|---|---|
| GET | Yes | Reads do not change state. |
| PUT | Yes | Replaces resource. Doing it twice gives the same final state. |
| DELETE | Yes | Once it is gone, deleting again is a no-op. |
| POST | No (by default) | Creates a new resource each time. |
| PATCH | Depends | "Set field to X" yes. "Increment field" no. |
The idempotency key pattern
For operations that are not naturally idempotent (POST, payments, order placement), the common pattern is the idempotency key. The client generates a unique ID, usually a UUID, and sends it in a header like Idempotency-Key. The server stores the result against that key. If the same key shows up again within some window (typically 24 hours), the server returns the stored result instead of repeating the operation.
How to actually implement it
The naive version is: check if key exists, if yes return stored response, if no do the work and store the response. The race condition is obvious — two requests with the same key arrive at the same time, both check, both miss, both do the work. The fix is an atomic insert with a unique constraint on the key column. Whichever insert wins does the work, the other gets a duplicate-key error and waits, then reads the stored response.
Stripe published their approach which is the de facto standard. They store the request fingerprint, the response, and the status. If a key is reused with a different request body, they reject it as a conflict. The window is 24 hours, which covers any reasonable retry scenario but lets keys eventually expire.
Idempotency at the database layer
Sometimes you can lean on the database. INSERT ... ON CONFLICT DO NOTHING in Postgres is idempotent. PUT in DynamoDB is idempotent. Using the natural primary key (like an order ID generated by the client) as the database key gives you idempotency for free.