Designing Idempotent APIs: A Practical Guide for Backend Engineers
Imagine a user clicking the “Submit Payment” button on an e-commerce site. The request goes out, but the user’s connection drops before they receive a response. Did the payment go through?
If the user clicks “Submit Payment” again, a poorly designed system might charge them twice. A well-designed system, however, will recognize the second request as a duplicate and handle it safely. This is the power of idempotency.
What is Idempotency?
In the context of APIs, an operation is idempotent if making multiple identical requests has the same effect as making a single request.
GET,PUT, andDELETEmethods are defined as idempotent by the HTTP specification. Deleting a resource that has already been deleted should return a200 OKor204 No Content(or a404 Not Found, but the system state remains the same).POSTmethods are not idempotent by default. If youPOST /api/ordersthree times, you typically get three separate orders.
In distributed systems, where network partitions, timeouts, and retries are a daily reality, making your critical POST endpoints idempotent isn’t just good practice—it’s mandatory.
The Solution: Idempotency Keys
The industry standard for solving this is the Idempotency Key. Here’s how it works:
- The client generates a unique identifier for the operation (usually a UUID v4).
- The client sends this identifier in an HTTP header (e.g.,
Idempotency-Key: <UUID>). - The server receives the request and checks its database to see if it has already processed this exact key.
- If not seen before: The server processes the request, saves the response associated with the key, and returns the response.
- If seen before: The server skips processing and simply returns the cached response from the original request.
A Practical Implementation Strategy
Let’s look at how to build this in a typical backend system.
1. The Database Schema
You need a fast, durable store for idempotency records. While Redis is tempting, a relational database (like PostgreSQL) is often better because you can wrap the idempotency check and the business logic in a single transaction.
CREATE TABLE idempotency_keys (
key VARCHAR(255) PRIMARY KEY,
user_id VARCHAR(255) NOT NULL,
request_path VARCHAR(255) NOT NULL,
status VARCHAR(50) NOT NULL, -- 'IN_PROGRESS', 'COMPLETED', 'FAILED'
response_body JSONB,
response_status INT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
2. The Logic Flow
When a request arrives with an Idempotency-Key:
- Attempt to insert the key with status
IN_PROGRESS. - Handle collisions: If the insert fails due to a unique constraint violation, it means the key exists.
- If the status is
COMPLETED, return the cachedresponse_bodyandresponse_status. - If the status is
IN_PROGRESS, return a409 Conflict(the original request is still processing, the client should back off and retry later).
- If the status is
- Execute business logic: If the insert succeeds, execute the actual operation (e.g., charge the credit card).
- Update the record: Once finished, update the idempotency record to
COMPLETEDand save the response payload.
3. Critical Edge Cases to Watch Out For
The User ID binding: An idempotency key must be scoped to a specific user or tenant. You don’t want User B guessing User A’s idempotency key and receiving their private payment response.
Payload validation: What if a client sends the same idempotency key but a different request body? Your system should hash the incoming request body and store the hash alongside the key. If a subsequent request has the same key but a different hash, reject it with a 400 Bad Request.
Expiration: Idempotency keys shouldn’t live forever. Typically, caching them for 24 hours is sufficient. Use a cron job or database TTL feature to clean up old records so your table doesn’t grow infinitely.
Conclusion
Building idempotent APIs adds a layer of complexity to your backend, but it completely eliminates an entire class of distributed system bugs. It allows clients to safely retry requests when the network is flaky, drastically improving the perceived reliability of your platform.
If your API deals with money, inventory, or any critical state mutations, idempotency isn’t an optional feature—it’s table stakes.