A practical guide to idempotency: the key to building resilient and trustworthy APIs
The silent killer in distributed systems
Ever held your breath after clicking “Confirm Purchase” on a spotty connection? We’ve all been there. The spinner spins, the network dies, and you’re left wondering, “Did my order go through? If I click again, will I be charged twice?” This is the problem idempotency solves, and it’s the difference between a junior developer’s API and a senior one.
Idempotency sounds like a dry academic term for “you can call the same request twice,” but it’s a practical design choice. You build the operation to survive retries from the start, instead of handling them after the fact.
The problem: duplicate requests, duplicate actions
Consider a junior engineer who builds a POST /create-order endpoint. A user clicks “purchase,” the client sends the request, and the network fails before the response arrives. The user clicks the button again, so the client sends another POST request. Now there are two orders and a customer who got charged twice. It’s a bug you can design out of the system.
The solution: designing for replayability
A senior engineer approaches the problem differently. Instead of a generic POST request, they build a PUT /order/{order-id} endpoint. The difference is that the client generates a unique ID (like a UUID) for the order before the first attempt.
Here’s how it works:
- The user clicks “purchase.”
- The client generates a unique order ID, for example,
xyz-123. - The client sends a
PUT /order/xyz-123request. - The server receives the request, creates the order with the ID
xyz-123, and returns a success response.
Now, let’s introduce some chaos. The network fails after the server has processed the request but before the client receives the success response. The user, seeing a timeout, clicks “purchase” again.
- The client, still holding onto the same unique order ID, sends another
PUT /order/xyz-123request. - The server receives the request and sees that an order with the ID
xyz-123already exists. - Instead of creating a new order, it simply returns the original success response.
The system’s state stays correct and the user isn’t charged twice. When an action is replayable, repeating it never changes the outcome, no matter how many times the client retries.
Implementing idempotency in your APIs
The concept is simple, but implementation details are where things go wrong. These practices cover the usual failure points:
- Use idempotency keys. For operations that are not naturally idempotent (like
POSTrequests), the client generates a unique key and sends it in a request header (e.g.,Idempotency-Key: <unique_value>). The server stores the key along with the response. If a request with the same key arrives again, the server returns the stored response without re-processing it. - Know which HTTP methods are idempotent.
GET,PUT, andDELETEare inherently idempotent;POSTandPATCHare not. - Add database constraints. A unique constraint on a transaction ID, for example, prevents duplicate records if your idempotency logic fails.
- Give keys an expiration. Set a Time-To-Live (TTL) so the server doesn’t store idempotency keys forever.
- Log key usage and failed attempts. When something looks wrong later, that history is what makes debugging fast.
Beyond the code: a shift in mindset
Idempotency is a mindset as much as a code pattern. When you’re designing an API, always ask yourself: “If this entire process ran again from scratch, would the outcome be exactly the same?” If the answer is no, you should be thinking about idempotency.
Built in from the start, this habit keeps systems predictable on the day a client actually retries, which is when it counts.