Marcio Cunha

API duplicated charges? How to ensure idempotency in backend

Learn how idempotency keys in REST APIs prevent double charges in payment gateways using relational databases to track requests.

Marcio Cunha•5 min
Also available in:PortuguêsEspañol
Summary
  • Duplicate requests happen due to network instability and automatic client retries.
  • Idempotency ensures an operation produces the exact same result even if executed multiple times.
  • Unique keys sent in HTTP headers identify every financial transaction unambiguously.
  • Relational databases with uniqueness constraints block unwanted parallel processing.
  • Proper HTTP status handling prevents visual and financial confusion for the end user.

The invisible danger of double charges in your API

Imagine the scene: a customer tries to buy a product on your e-commerce store, the payment button freezes for two seconds, they click again, and at the end of the month, they notice they were charged twice. In software engineering, we call this anomaly duplicate request processing. When the network drops halfway through, the client application does not know if the payment reached the server and tries to send the order again. If your backend is not prepared to recognize that this is the exact same purchase, the system will process the payment twice. In practice, this means support headaches and financial loss.

To make matters worse, payment gateways, which are the external systems handling credit cards, also have their own retry rules. If your microservice triggers a payment request and suffers a connection drop before receiving a response, it remains in the dark. Upon reconnecting, if it tries to charge again without a protection strategy, the damage is done. It is precisely to shield the system against these failures that we use the concept of idempotency, an elegant computing word that solves a very everyday problem.

What idempotency is and how it works in practice

Simply put, an idempotent operation is one that you can repeat as many times as you want, but the final outcome is always identical to the first execution. Think of an elevator button: if you press the lobby button five times in a row, the elevator goes to the lobby and stays there, without descending another five floors. On the web, the HTTP GET method is naturally idempotent because fetching a page ten times just brings the same information back without altering anything in the database. The problem lies in POST methods, which create records and perform financial actions.

When we build a REST API to receive payments, we need to inject this same safety logic. The most efficient tool for this is the idempotency key, known internationally as an Idempotency Key. It works like this: before sending the payment order, the client application generates a unique code, such as a random universally unique identifier (UUID). This code travels in the HTTP request header. When your server receives the payment request, it quickly queries the database to check if this code has already been processed before.

Designing the validation flow in a relational database

To implement this safety lock, we need a dedicated table in a relational database like PostgreSQL or MySQL. This table stores the idempotency key, the current status of the request, which can be 'processing', 'success', or 'failure', and the original response returned to the client. When a new request arrives, the first thing the code does is attempt to insert this key into the table with the initial status. Using uniqueness constraints in the database ensures that even if two identical requests arrive in the exact same millisecond, only one will successfully write the record.

If the insertion fails because the key already exists, the backend enters recovery mode. If the original request is still running, the server can reply with an appropriate HTTP code informing that the operation is in progress, avoiding a new heavy parallel execution. If the previous operation already finished successfully, the server simply fetches the saved response and returns it intact to the client, pretending it executed everything over again without touching the payment gateway. This strategy protects data integrity and optimizes server usage.

CREATE TABLE idempotency_keys (
    key VARCHAR(255) PRIMARY KEY,
    status VARCHAR(50) NOT NULL,
    response_body JSONB,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

Handling concurrency and the danger of race conditions

One of the biggest challenges when implementing this architecture is dealing with high-scale concurrency. Race conditions happen when two requests with the same key arrive almost together and pass the existence check simultaneously before either of them can save the record in the table. If this happens, both will try to call the payment gateway and duplicate charging will occur anyway.

The safest way to shield your code against this flaw is to rely on the primary key or unique index constraint of the relational database itself. Instead of doing a SELECT followed by an INSERT in separate moments, execute the insert command directly. The database will reject the second command instantly due to key duplication, throwing a controlled error. Your code catches this error and decides whether to wait for the first request to finish or return the already cached result. Just like when Node.js leaks memory under heavy load and requires resilience care, payment architecture demands structural rigor.

Best practices for expiring keys and maintaining API health

Storing idempotency keys forever in the database is an unnecessary waste of disk space and can degrade query performance over the months. Companies usually adopt a validity window for these keys, ranging between 24 hours and 7 days, depending on the financial product's business rule. After this period, old keys can be safely deleted by an automated cleanup routine, a process similar to when we clean the systemd journal cache on Linux to free up disk space.

Another critical point is the proper handling of HTTP status codes in cached responses. If the original transaction succeeded, the repeated response must return the exact same success code, such as 200 OK or 201 Created. If the operation failed due to insufficient funds, the system must return the original error, ensuring the client understands the real reason without generating a rogue retry. Transparency in status codes prevents confusion for both client applications and technical support teams.

Conclusion

Ensuring idempotency in payment APIs is not just an implementation detail, but a fundamental reliability requirement for any modern backend system. By using unique keys tied to strict constraints in relational databases, we eliminate the risk of duplicate charges caused by network drops and automatic retries. Investing time in this architecture protects customer assets, strengthens company reputation, and drastically reduces operational support tickets.

Adopting this pattern requires discipline in data modeling and attention to concurrency details, but the benefits far outweigh the initial effort. With a secure, transparent, and resilient flow, your application gains maturity to scale with stability and confidence, ready to absorb traffic spikes without financial scares.