Practice this topic in a realistic system design interview
An operation is idempotent if performing it multiple times has the same intended effect as performing it once. Idempotency is what lets an API safely handle retries without performing the same operation multiple times.
Loading simulation...
In this chapter, we will look at idempotency, why it matters, and how to design APIs that can safely handle retries without performing the same operation multiple times.
Let's start with a simple example. Suppose a user is making a payment for ₹1,000.
The client sends a request to the payment service. The payment service processes the charge successfully, but before the response reaches the client, the network connection fails.
The client cannot tell what happened. Maybe the payment succeeded. Maybe it failed. Maybe the server never received the request.
So the client takes the standard approach: it retries the request. But if the first request actually succeeded, retrying the same request could charge the user twice.
This is the problem idempotency is designed to solve.
An operation is idempotent if performing it multiple times has the same intended effect as performing it once. The important part is the effect on the system.
For example, suppose we send a request saying: set the user's status to active.
If we execute that operation once, the status becomes active. If we execute it again, the status is still active. The final state is the same. So this operation is naturally idempotent.
Now compare that with: add $10 to the user's balance.
If we perform it once, we add $10. If we perform it twice, we add $20. The outcome changes every time the request is repeated. That operation is not naturally idempotent.
Some operations are naturally idempotent. For example:
SHIPPED.123.If the same operation is repeated, the system ends up in the same state.
This is one reason HTTP methods such as PUT and DELETE are generally designed to be idempotent. A request like this:
means: make this resource look like this. Sending that request multiple times should leave the resource in the same final state.
Similarly, deleting the same resource repeatedly should not delete additional resources. After the first successful deletion, later requests may return a different status code, but the intended effect is still the same.
Other operations are not idempotent by default. Creating a new order is a common example.
Suppose the client sends POST /orders. The server creates order 1001. If the client sends the exact same request again, the server might create order 1002. The same logical action has now happened twice.
| Request | Server result |
|---|---|
POST /orders (first attempt) | Creates order 1001 |
POST /orders (retry) | Creates order 1002 |
Payments have the same problem. A request saying charge this card ₹1,000 should usually happen once. Repeating it should not create another charge.
So when an operation is not naturally idempotent, we need to add a mechanism that lets the server recognize repeated attempts.
A common solution is an idempotency key. An idempotency key is a unique identifier generated by the client for a logical operation.
Suppose a user clicks the Pay button. Before sending the payment request, the client generates a unique key, for example payment:7842. The client sends that key with the request, usually in a header:
The server stores the key along with the result of processing the operation.
If the request succeeds and the client receives the response, the operation completes normally.
But suppose the response is lost and the client retries. The client sends the same request with the same idempotency key. The server sees that this key has already been processed. Instead of charging the user again, it returns the result of the original request.
This makes retries safe.
Because the key identifies the logical operation, the client must create it before the first attempt and reuse it on every retry. If the client generated a new key for each retry, the server would see each attempt as a new operation.
Let's walk through the flow in more detail.
If the same request arrives again with the same key, the server looks up the stored record. Instead of executing the payment again, it returns the previous result.
For each key, the server stores a record with these fields:
| Field | Example |
|---|---|
| Idempotency key | payment:7842 |
| Request | charge ₹1,000 |
| Processing status | IN_PROGRESS or COMPLETED |
| Response | 201 Created, payment pay_581 |
That record allows the server to determine whether a request is new, still in progress, or already completed.
An important detail is that the same idempotency key should only be reused for the same logical request.
Suppose the client sends a payment request for ₹1,000 using idempotency key A. Later, it sends another request with the same key, but this time for ₹2,000. The server should not silently treat those as the same operation.
A common approach is to store a hash or some important fields from the original request alongside the idempotency key. When a duplicate request arrives, the server compares the new request with the original one.
| Request | Key | Request hash | Result |
|---|---|---|---|
| Charge ₹1,000 | A | h1 | Processed |
| Charge ₹1,000 (retry) | A | h1 | Original result returned |
| Charge ₹2,000 | A | h2 | Rejected: key reused with a different request |
If the parameters are different, it rejects the request. This prevents accidental reuse of the same key for two different operations.
There is another problem. What happens if two requests with the same idempotency key arrive at almost the same time?
Imagine the server checks whether the key exists:
Request A checks first. The key does not exist. At nearly the same moment, Request B checks. The key still does not exist. Both requests then begin processing.
Now we have exactly the duplication problem we were trying to prevent.
So checking and inserting the key must be handled atomically. For example, the server might use a database constraint that allows only one row for each idempotency key:
The first request successfully creates the record. The second request sees that the key already exists and does not execute the operation again.
This is why idempotency is not just about storing a key. Checking and reserving that key must stay correct under concurrent requests.
Now consider another case. The first request is still being processed when a retry arrives. The idempotency record exists, but there is no final result yet.
The server has several options:
| Option | What the client sees |
|---|---|
Return 202 Accepted | The request is still being processed. |
| Wait briefly | The server holds the retry until the original request completes, then returns its result. |
Return 409 Conflict | The operation is already in progress. |
The exact behavior depends on the API. The important thing is that the second request should not start another copy of the operation.
Once the original request completes, future retries can return the stored result.
Idempotency records need to be stored somewhere durable enough for the operation being protected. A database is a common choice.
The server might store:
Storing the owner with the key means two different clients that happen to pick the same key string do not collide.
A fast distributed store such as Redis can also be used in some systems. But there is an important trade-off. If losing the idempotency record could lead to a duplicate payment, the storage needs to provide appropriate durability and consistency.
A cache that can lose data should not be the only protection for a financial operation. So the storage mechanism should match the importance of the side effect being protected.
Idempotency records do not necessarily need to be stored forever. They usually have a retention period.
For example, an API might retain idempotency keys for 24 hours. During that period, retries with the same key return the original result. After the record expires, the key may be treated as new.
The appropriate retention period depends on how long clients are expected to retry. A few minutes may be enough for some internal operations. A payment API may retain keys much longer.
Long retention improves protection against late retries, but it also increases storage requirements. So this is another design trade-off.
One of the harder parts of idempotency is keeping the actual operation and the idempotency record consistent.
Suppose we create an order in the database. Then the server crashes before storing the idempotency result. The order exists, but the idempotency record does not show that the request completed. When the client retries, the server may create another order.
One solution is to store the business operation and the idempotency record in the same database transaction when possible:
Create the order. Mark the idempotency key as completed. Commit both changes together.
Now either both happen or neither happens. This greatly reduces the chance that the business state and idempotency state become inconsistent.
There is another subtle point. An idempotent operation does not always have to return exactly the same HTTP response every time.
Suppose we delete a resource:
| Request | Response | Resource state |
|---|---|---|
DELETE /users/123 (first) | 200 OK | Deleted |
DELETE /users/123 (second) | 404 Not Found | Still deleted |
The response changed. But the state of the system did not. The resource is still deleted.
So when we talk about idempotency, we mainly care about the intended side effect, not whether every response is byte-for-byte identical.
That said, APIs using explicit idempotency keys often choose to return the original response because it makes retries easier for clients.
PUT and DELETE are designed to be idempotent. POST is not by default.202, wait briefly, or return 409.10 quizzes