Idempotency

Explains idempotency in API design with practical examples, showing how HTTP methods, idempotency keys, request hashes, and atomic database writes work together to prevent duplicate operations in systems like payments and e‑commerce.

Idempotency is one of those terms that gets thrown around in interviews, but if you work with APIs, it’s genuinely important. Understanding it helps you build safer systems and improves user experience especially when real-world networks are unreliable.

Today, most of the web is accessed through mobile phones on LTE/5G. Even though these networks are fast, connectivity isn’t always stable. Requests can fail mid-flight, devices can reconnect automatically, or apps can lose signal for a few seconds without the user realizing it.

Behind the scenes, mobile and web apps usually have retry mechanisms to handle these transient failures. Retries are helpful but they also create a major challenge: how do you ensure the backend doesn’t process the same action twice?

Idempotency defined

As per the IETF definition:

A request method is considered idempotent if the intended effect on the server of multiple identical requests with that method is the same as the effect for a single such request.

In simple terms: if you send the same request multiple times, the server should behave as if it was executed only once.

Let’s map this idea to common HTTP methods: GET, POST, PUT, PATCH, DELETE (and OPTIONS).

HTTP methods and idempotency

  • GET – Used to fetch a resource. Calling it multiple times should return the same result (assuming the resource hasn’t changed).
  • DELETE – Deleting the same resource multiple times results in the same end state: the resource is gone.
  • PUT – Replaces or updates a resource at a known identifier. Updating a customer’s email by customer ID produces the same final state, even if repeated.
  • PATCH – Applies partial updates. Some PATCH operations are idempotent, but not all. For example, “set status to ACTIVE” is idempotent, but “increment balance by 100” is not.
  • POST – Usually creates a new resource. Repeating the same POST commonly creates duplicates (e.g., duplicate payments/transactions), so POST is not idempotent by default.

Why enforcing idempotency matters ?

Idempotency becomes critical in systems where correctness matters more than convenience—think e-commerce, banking, and payments.

If a user initiates a payment and the request fails due to a network drop, the client will retry. Without idempotency safeguards, that retry can create a second transaction—which is unacceptable in financial systems.

Enforcing idempotency: client + server

A robust solution needs work on both sides.

Client-side

The client should send a unique idempotency key with the request, commonly via a header like:

  • X-Request-Id (or Idempotency-Key)

This ID must be:

  • unique per business action (one payment attempt, one checkout, one fund transfer)
  • reused across retries of the same action

Server-side

The server should store and validate this idempotency key before processing:

  • If the key is new, process the request and record the result.
  • If the key was already seen, do not execute again—either:
    • return the same response as earlier, or
    • return a 409 Conflict (depending on API design)

What is a request hash?

A request hash is a fingerprint of the request payload (and sometimes key headers).
Example: compute SHA-256 of a canonical form of the request body.

So you store something like:

  • idempotency_key (X-Request-Id / Idempotency-Key)
  • request_hash (hash of request details)
  • status (IN_PROGRESS / SUCCESS / FAILED)
  • response_body + response_code (optional but powerful)

Why do we need a request hash?

1) Prevents “same key, different request” bugs

Without a request hash, a client (or a buggy retry flow) could reuse the same X-Request-Id but change the payload:

  • Retry #1: amount=1000
  • Retry #2: amount=10000 (maybe UI bug / user edited / client bug)

If the server only checks X-Request-Id, it may incorrectly treat the second request as a retry and return the first response — or worse, process the wrong thing.

With a request hash:

  • If idempotency_key matches but request_hash differs → reject with 409 Conflict (or 422), because it’s not the “same request”.

2) Protects against accidental duplicates across clients

Even if you scope uniqueness by (clientId, idempotency_key), hashes add another safety check:

  • if two clients somehow collide on the same key, the hash mismatch will reveal it.

3) Makes idempotency auditable

When you store the hash, you can later prove:

  • “This payment was retried 4 times but payload stayed the same”
  • “This key was reused with a modified payload → blocked”

4) Helps safe caching of the previous response

A common best practice is: “same key → return same response”
But you only want to do that if the request is truly identical.
The request hash is what gives you the confidence to replay the same stored response.


What should be included in the request hash?

Include fields that define the business intent, such as:

  • amount, currency
  • senderAccountId / payerId
  • receiverAccountId / merchantId
  • orderId / cartId
  • operation type (PAYMENT/REFUND)
  • any idempotency “scope” fields like clientId / userId

Avoid unstable fields:

  • timestamps generated by the client
  • tracing headers
  • random metadata

Also ensure the JSON is canonicalized (stable ordering) before hashing, otherwise the same JSON with different key order could produce different hashes.

How it changes server behavior

When a request comes in:

  1. Look up (scope + idempotency_key)
  2. If not found → insert row with request_hash, mark IN_PROGRESS, process
  3. If found:
    • if request_hash matches:
      • if SUCCESS → return stored response
      • if IN_PROGRESS → return 202 Accepted (or wait/poll pattern)
      • if FAILED → either allow retry or return failure consistently (your choice)
    • if request_hash differs → return 409 Conflict (“Idempotency key reuse with different request payload”)

Atomic transaction

All of the above only works reliably if the idempotency check and the actual write happen atomically.

That means:

  • the “check if request-id exists” and “insert transaction” must be part of the same database transaction, or
  • enforced using a unique constraint so duplicates fail safely, even under race conditions.

This protects you when two retries hit the server at the same time.

srnyapathi
srnyapathi
Articles: 41