
April 4, 2026
Understanding Idempotency in APIs : Preventing Duplicate Requests
Imagine you are making a payment on an e-commerce website.
You click "Pay Now", and the payment request is sent to the server.
But due to a slow network or server delay, the response does not arrive immediately.
You start wondering whether the payment went through or not, so you click "Pay Now" again.
Now the server receives the same request twice.
If the system is not designed carefully, this may create:
- two payments
- two orders
- inconsistent transaction records
This is a very common problem in distributed systems where requests may be retried due to:
- network timeouts
- slow server processing
- unstable mobile networks
- users clicking the same button multiple times
So how do we make sure that retrying the same request does not create duplicate results?
This is where Idempotency comes into the picture.
In the previous article, we understood how idempotency helps in event-driven systems when the same message can be delivered more than once.
In this article, we will look at the same idea from the API side.
- understand the problem of duplicate requests\
- learn what idempotency means in APIs\
- and implement a simple solution using a Spring Boot application
The Problem: Duplicate Requests
Let us understand the problem a little more clearly.
Suppose a user is paying for an order.
The client sends the request:
POST /payments with the payload:
{
"orderId": "ORD-101",
"amount": 500
}
But before the response arrives, the client thinks something went wrong and retries the request.
Now the server receives:
POST /payments
If the system simply processes every request, we may end up creating two payments for the same order.
This is obviously not acceptable for systems dealing with payments or financial transactions.
We need a way to make sure that retrying the same request does not create duplicate results.
What is Idempotency?
A simple definition is:
Idempotency ensures that retrying the same API request does not create duplicate results.
In other words, if the same request is sent multiple times:
- the server should process it only once
- all subsequent retries should return the same result
This is especially important for APIs that deal with:
- payments
- order creation
- financial transactions
How Idempotency Works
One common way to implement idempotency in APIs is by using an Idempotency Key.
The client sends a unique key with the request.
Example:
POST /payments
Idempotency-Key: abc-123
The server stores this key along with the result of the request.
If the same key appears again, the server understands:
This request was already processed earlier.
Instead of creating a new payment, the server simply returns the previous result.
Example Implementation
To understand this better, we have built a small application using Spring Boot.
API Implemented
POST /payments
Request body:
{
"orderId": "ORD-101",
"amount": 500
}
Header:
Idempotency-Key: abc-123
The application uses two database tables.
payments table
Stores the actual payment.
id | paymentRef | orderId | amount | status | createdAt
Example:
1 | PAY-1001 | ORD-101 | 500 | SUCCESS
idempotency records table
Stores idempotency metadata.
idempotencyKey | requestHash | paymentId | createdAt
Example:
abc-123 | hash-value | 1 | 2026-03-14 19:16:59.227342
The important design decision is that idempotencyKey has a unique constraint.
This ensures one key maps to only one logical request.
Why We Store a Request Hash?
The idempotency key alone is not enough.
We must also verify that the repeated request has the same payload.
In our demo, we generate a SHA-256 hash from:
orderId | amount
Example:
ORD-101|500
This generates a deterministic hash.
This hash is stored along with the idempotency key.
When the same request arrives again:
- if the key exists
- and the hash matches
then it is treated as a valid retry.
But if the hash is different, it means the same key is being reused for a different request, which should not be allowed.
Note: In this application, we are using an in-memory H2 database to keep the implementation simple and easy to run locally.
In real production systems, idempotency records are usually stored in a persistent database (like PostgreSQL/MySQL) or in a distributed cache such as Redis.
When Redis is used, idempotency keys are often stored with a TTL (Time To Live) so that old keys automatically expire after a certain period.
This helps prevent the idempotency store from growing indefinitely.
Request Flow (Step by Step)
Let us see how the API works internally.
flowchart TD
A[Client Sends Request<br>POST /payments<br>Idempotency-Key] --> B[PaymentController]
B --> C[PaymentService]
C --> D{Check Idempotency Key<br>in idempotency_records}
D -->|Key Not Found| E[Create New Payment]
E --> F[Save Idempotency Record<br>Key + Request Hash + PaymentId]
F --> G[Return Payment Response<br>201 Created]
D -->|Key Exists| H{Compare Request Hash}
H -->|Hash Matches| I[Fetch Existing Payment]
I --> J[Return Existing Response<br>200 OK]
H -->|Hash Different| K[Reject Request]
K --> L[409 Conflict]
First Request
- Client sends
POST /payments - Client includes an
Idempotency-Key - Controller validates the request
- Service generates a request hash
- System checks the
idempotency_recordstable - No record exists
- A new payment is created
- A new idempotency record is stored
- Payment response is returned
Duplicate Request with Same Key
If the same request is sent again:
- The service generates the same request hash
- The idempotency record is found
- Stored hash matches the new hash
- The existing payment is fetched
- The same response is returned
No new payment is created.
Same Key with Different Payload
Example.
First request:
Idempotency-Key: abc-123 amount: 500
Second request:
Idempotency-Key: abc-123 amount: 700
Now the hash will be different.
The system detects this conflict and rejects the request.
Response:
409 Conflict
This ensures the same key cannot be reused incorrectly.
Testing the API with Postman
Scenario 1 --- New Key
Header:
Idempotency-Key: abc-123
Response:
201 Created
A new payment is created.

Scenario 2 --- Same Key, Same Payload
Send the same request again.
Response:
200 OK
The API returns the same payment.
No new row is created in the database.

Note: The status code is not 201, its 200 OK
Scenario 3 --- Same Key, Different Payload
Change the amount but keep the same key.
Response:
409 Conflict
The request is rejected.

Note: Here, the request body(orderId) changed, due to which the previous and the current request hash does not match, for same idempotencyKey.
Conclusion
Idempotency is an important concept when designing reliable APIs.
It ensures that:
- retrying requests does not create duplicate results
- APIs remain safe under network failures and timeouts
In our implementation:
- the idempotency key identifies the logical request
- the request hash verifies payload consistency
- the idempotency table links the request to the created payment
Together, these ensure that retries return the same result instead of creating duplicates.
If you want to explore the full working demo, the source code is available here: backend-patterns-and-practices/idempotency-in-apis.