Preventing Duplicate Payments with Idempotency in NestJS
Prevent double payment in payment service using technique called idempotency

Common issue in payment service
One of the most common and dangerous problems in payment systems is duplicate transactions.
Imagine this scenario:
- A user clicks “Pay Now”.
- The request is sent to the server.
- The payment succeeds, but the network response is slow.
- The user thinks the request failed and clicks “Pay Now” again.
Without proper safeguards, the system may process two payments instead of one. This is where idempotency becomes essential.
What is idempotency
In system design, idempotency means performing the same operation multiple times produces the same result.
For example:
If the same request is sent multiple times, the server should only process it once. Instead of charging the user twice, the server should return the same response from the first request. This is usually implemented using an Idempotency Key.
An Idempotency Key is a unique identifier generated by the client and sent with a request.
Example request:
If the server receives the same key again, it does not process the payment again. Instead, it returns the previous response. This ensures the operation is safe to retry.
Many payment platforms use this pattern, including:
- Stripe
- PayPal
- Square
Architecture Overview
A typical idempotency workflow looks like this:
Idempotency workflow
The key idea is to store the result of the first request. If the same request arrives again, return the stored result instead of executing the operation again.
Implementing Idempotency in NestJS
Now that we understand the concept of idempotency, let's implement it in a real backend using NestJS. The goal is simple:
If the same request with the same idempotency key arrives multiple times, the server should execute the operation only once and return the same response for subsequent requests.
To achieve this, we will build a small system consisting of:
- A database table to store idempotency records
- A service layer to interact with the stored keys
- A NestJS interceptor to handle the idempotency logic
- A controller endpoint to demonstrate the behavior
This architecture allows idempotency to be reused across multiple endpoints without polluting business logic.
Step 1 — Create an Idempotency Table
The first thing we need is persistent storage for idempotency keys. When a request with an idempotency key is processed, we want to store:
- the idempotency key
- the response body
- the HTTP status code
- timestamp
This allows the server to replay the exact same response if the same key appears again. Example SQL schema:
Step 2 — Create an Idempotency Service
Next, we create a service layer responsible for interacting with the database. Example implementation:
The find method checks if a request with the same key has already been processed. Example usage:
If a record exists, we know that this operation was already executed before.
The save method stores the result of the request after it has successfully completed. It saves:
- the idempotency key
- response body
- status code
This is important because the next request should return the exact same response, including HTTP status.
Step 3 — Create an Idempotency Interceptor
Now comes the core of the implementation. In NestJS, interceptors allow us to run logic:
- before the request handler executes
- after the response is generated
This makes them perfect for implementing idempotency. We want the interceptor to:
- Check if the request contains an
Idempotency-Key - Look up the key in the database
- If the key exists → return stored response
- If not → execute the request
- Save the response for future retries
Implementation:
Step 4 — Apply the Interceptor to an Endpoint
Now we apply the interceptor to a payment endpoint.
Now if the client sends this request:
The server processes the payment and returns:
The result is stored in the database.
In retry scenario, if the same request is sent again:
Instead of running the payment logic again, the server returns the stored response. The payment logic runs only once.
Step 5 — Client-Side Implementation
The final step is ensuring the client generates a unique key. Example using JavaScript:
But, the important rule is the same key must be reused when retrying the request. This ensures the server treats retries as the same operation.
Conclusion
Idempotency is a crucial technique for building reliable and safe APIs, especially for operations like payments, order creation, or other financial transactions. In real-world systems, network issues, client retries, or accidental double-clicks can easily cause the same request to be sent multiple times. Without proper protection, this could result in duplicate operations, such as charging a user twice.
By implementing idempotency in NestJS, we can ensure that repeated requests with the same idempotency key will return the same result, instead of executing the operation again. Using a combination of a stored idempotency key, a service layer, and a NestJS interceptor, we can add this protection without complicating our business logic.
Production Considerations
When implementing idempotency in production, keep these best practices in mind:
- Use TTL for idempotency keys Store keys only for a limited time (for example 24 hours) to prevent unnecessary database growth.
- Prevent race conditions Use database unique constraints, atomic inserts, or locks to ensure two identical requests cannot process simultaneously.
- Validate request payloads Reject requests that reuse the same idempotency key with a different payload.
- Apply idempotency only where necessary Focus on critical endpoints such as payments, order creation, or subscription processing.
- Consider Redis for high-scale systems For very high traffic APIs, using an in-memory store like Redis can improve performance.
In short, idempotency makes APIs safe to retry and resilient to failures. Implementing it early in your backend architecture can prevent serious issues like duplicate payments and inconsistent data as your system grows.
Thank you for reading! If you found this helpful, feel free to share it.