Idempotency_in_APIs_Praeclarum_Tech_feature_image

Idempotency in APIs: Preventing Duplicate Payments and Orders



Idempotency in APIs: Preventing Duplicate Payments and Orders

Overview / Introduction

Modern applications communicate over networks that are not perfectly reliable. A customer may click a Pay button twice, a mobile application may retry after a timeout, or an API gateway may resend a request because it never received a response. If the backend treats every repeated request as new, one business action can accidentally become two payments, two orders, two refunds, or two account transfers.

Idempotency is an API design technique that makes repeated execution of the same logical request safe. For operations where duplication can cause financial or operational damage, the client sends a unique idempotency key. The server uses that key to recognize retries and returns the result of the original operation instead of performing the operation again.

This blog explains idempotency from a backend engineering perspective, with payment and order examples. It covers request flow, data storage, concurrency, database constraints, expiration, benefits, drawbacks, and a practical Node.js-style implementation.

Why Duplicate Requests Happen

• A user double-clicks a payment or order button.

• The client times out even though the server completed the operation.

• A mobile network disconnects after the server commits data but before the response reaches the device.

• Automatic retry logic in a client, gateway, queue, or service sends the same command again.

• Two application instances receive nearly simultaneous copies of the same business request.

A Simple Failure Scenario

Assume a customer submits an order for ₹5,000. The backend successfully creates the order, but the response is lost. The client cannot know whether the request failed before or after processing, so it retries. Without protection, the second request can create another order and possibly trigger another payment. The technical failure is not the retry itself; retries are often necessary. The problem is a backend that cannot recognize that both requests represent the same intended action.

What Is Idempotency?

An operation is idempotent when performing the same logical operation multiple times has the same intended effect as performing it once. In an API, this does not necessarily mean that every response is byte-for-byte identical or that no logs are added. It means the protected business side effect is not repeated.

Operation Typical Behavior Idempotency Concern
GET /orders/123 Reads an existing resource Naturally safe when it only reads data
PUT /profile/123 Sets a resource to a requested state Repeating the same update normally keeps the same state
DELETE /cart/123 Removes a resource Repeated calls should leave the resource deleted
POST /payments Creates a new financial action Dangerous if a retry creates another payment
POST /orders Creates a new order Dangerous if a retry creates another order

What Is an Idempotency Key?

An idempotency key is a unique value that identifies one logical operation. The client generates the key before sending a sensitive request and reuses exactly the same key when retrying that request. A different business operation must use a different key.

POST /api/payments HTTP/1.1
Content-Type: application/json
Idempotency-Key: 8f6d2b6e-7b34-4a66-a8f8-0b70f20f6a11

{
  "orderId": "ORD-10452",
  "amount": 5000,
  "currency": "INR"
}

Recommended Key Properties

• Unique enough to avoid accidental collisions; UUID-style values are a common choice.

• Generated once for the logical action and retained for retries.

• Scoped appropriately, for example by account or API operation when required.

• Not derived only from timestamps, which can collide or be regenerated on retry.

• Treated as an identifier, not as authentication or authorization.

The Core Rule

The server must associate the key with the request and its outcome. When the same key arrives again, the backend should not repeat the protected side effect. It should return the stored outcome, or a clear in-progress/conflict response if the first request has not finished yet.

How the Backend Flow Works

The following original flow illustrates a common implementation. The exact storage technology can vary, but the decision point must happen before the protected operation is repeated.

Idempotency_in_APIs_Praeclarum_Tech_Blog

Figure 1. Idempotent request flow for a payment or order API.

Step-by-Step Request Lifecycle

• 1. The client creates an idempotency key and sends it with the request.

• 2. The API authenticates the caller and validates the payload.

• 3. The backend checks its idempotency store for the key.

• 4. If a completed record exists, the backend returns the previously stored result.

• 5. If no record exists, the backend atomically reserves the key or creates an in-progress record.

• 6. The business operation executes once, ideally inside an appropriate transaction.

• 7. The backend stores the final status and response against the key.

• 8. Any later retry with the same key receives the stored result instead of triggering the operation again.

Suggested Idempotency Record

Field Purpose
key Unique identifier supplied by the client
requestHash Fingerprint used to detect reuse of the same key with a different payload
status IN_PROGRESS, COMPLETED, or FAILED according to the chosen policy
responseCode HTTP status returned by the original operation
responseBody Stored response or enough information to reconstruct it
resourceId Created payment/order identifier
createdAt / expiresAt Retention and cleanup timestamps

Practical Backend Example

The example below is intentionally framework-friendly rather than tied to a specific production database. The important behavior is the sequence: validate the key, reserve it atomically, perform the operation once, then persist the result.

app.post('/payments', async (req, res) => {
  const key = req.get('Idempotency-Key');
  if (!key) return res.status(400).json({ message: 'Idempotency key required' });

  const requestHash = hashRequest(req.body);
  const existing = await idempotencyStore.find(key);

  if (existing) {
    if (existing.requestHash !== requestHash) {
      return res.status(409).json({ message: 'Key reused with different request' });
    }
    if (existing.status === 'COMPLETED') {
      return res.status(existing.responseCode).json(existing.responseBody);
    }
    return res.status(409).json({ message: 'Request is already being processed' });
  }

  await idempotencyStore.reserveUnique(key, requestHash);
  const payment = await paymentService.create(req.body);
  const body = { id: payment.id, status: payment.status };
  await idempotencyStore.complete(key, 201, body);
  return res.status(201).json(body);
});

Why a Simple “Check Then Insert” Is Not Enough

Two requests with the same key can arrive at almost the same time. Both may check the database before either has inserted the key. If both see “not found,” both can continue and create duplicate side effects. This is a race condition. The reservation step therefore needs an atomic guarantee, commonly a unique database constraint or another compare-and-set style operation.

CREATE TABLE api_idempotency (
  idempotency_key VARCHAR(100) PRIMARY KEY,
  request_hash VARCHAR(128) NOT NULL,
  status VARCHAR(20) NOT NULL,
  response_code INT,
  response_body TEXT,
  created_at TIMESTAMP NOT NULL,
  expires_at TIMESTAMP NOT NULL
);

Idempotency and Database Transactions

Transactions and idempotency solve related but different problems. A transaction protects consistency inside one execution: either a set of database changes commits together or it rolls back. Idempotency protects the system across repeated executions of the same logical request. A robust payment or order workflow often needs both.

Important Design Decisions

1. Same Key, Different Payload

A client should not be allowed to reuse one idempotency key for a different operation. Storing a normalized request hash makes this detectable. If the key matches but the payload hash differs, the API can reject the request with a conflict response rather than silently returning an unrelated result.

2. What Should Happen While the First Request Is Running?

When a duplicate arrives while the first request is still in progress, the API can reject the duplicate as “already processing,” wait for the original operation for a bounded period, or return a status that tells the client to retry later. The chosen behavior should be documented and consistent.

3. How Long Should Keys Be Stored?

Idempotency records should not necessarily live forever. Retention depends on the maximum realistic retry window and business requirements. A short TTL reduces storage but may allow a very late retry to be treated as new. A long TTL provides a larger safety window but increases storage and cleanup requirements.

Where to Use Idempotency

Use Case Risk Without Idempotency Typical Protected Action
Payment creation Customer can be charged twice Create payment/charge
Order creation Duplicate orders and inventory reservations Create order
Refund Customer may receive multiple refunds Create refund
Money transfer Balance may be moved more than once Initiate transfer
Subscription creation Multiple subscriptions can be created Create subscription
External lender/provider submission Same application may be submitted repeatedly Submit enrollment/application

When Idempotency May Be Unnecessary

Not every endpoint needs an idempotency store. Read-only endpoints normally do not create duplicate side effects. Some state-setting operations are already naturally idempotent. Adding key storage to every endpoint can create unnecessary complexity, so the mechanism is most valuable where repeated side effects are expensive, irreversible, or difficult to reconcile.

Security and Operational Considerations

• Authenticate and authorize the request normally; an idempotency key never replaces security checks.

• Avoid storing sensitive payment or personal data unnecessarily in the idempotency response record.

• Scope keys so one tenant or user cannot accidentally collide with another when the system design requires isolation.

• Monitor duplicate-hit rates, conflicts, expired keys, and stuck IN_PROGRESS records.

• Define retry behavior for downstream providers because your API can be idempotent while a downstream integration is not.

Benefits and Drawbacks

Benefits Drawbacks / Trade-offs
Prevents duplicate business side effects during retries. Requires additional storage and cleanup logic.
Makes client retries safer after timeouts or network failures. Concurrency must be handled correctly; naive check-then-insert logic can still race.
Improves reliability for payment, order, refund, and transfer APIs. Storing responses or metadata increases operational complexity.
Provides a traceable identifier for one logical operation. Key expiration policies must balance safety window and storage.
Can improve user trust by avoiding accidental duplicate actions. Incorrect key reuse by clients must be detected and handled.

Best Practices

• Require idempotency keys on high-risk create operations rather than treating them as an optional convention.

• Use a database unique constraint or equivalent atomic reservation to handle concurrent duplicates.

• Bind the key to the request payload using a deterministic hash.

• Store the result needed to reproduce the original response or reliably reconstruct it.

• Define behavior for failures: decide which errors are retryable and whether failed attempts release or retain the key.

• Use TTL/expiration and a cleanup process appropriate to the business retry window.

• Keep idempotency logic close to the API boundary, but keep business transactions and downstream guarantees explicit.

• Test double-clicks, client timeouts, concurrent requests, service restarts, and delayed retries – not only the happy path.

Idempotency vs. Related Techniques

Technique Primary Purpose Does It Prevent Duplicate API Effects by Itself?
Idempotency key Recognize repeated logical requests Yes, when implemented correctly
Database transaction Keep one execution atomic and consistent No
Unique business constraint Prevent duplicate values/resources Sometimes, but not a complete API retry strategy
Rate limiting Control request frequency No
Message deduplication Suppress repeated message processing Yes for the protected message flow, with its own scope and retention

Conclusion

Retries are a normal part of distributed systems, but duplicate payments and orders should not be. Idempotency gives a backend a reliable way to distinguish a retry from a new business action. The core pattern is straightforward: identify the logical request with a stable key, reserve that key atomically, execute the side effect once, and reuse the stored result for duplicates.

For fintech and commerce systems, the strongest implementation combines idempotency with database constraints, transactions, careful downstream integration, request hashing, expiration policies, and concurrency testing. The result is not merely a cleaner API; it is a safer user experience when networks, clients, and services behave unpredictably.