Idempotency in Practice: Handling the Same Key with Different Parameters
The article explains why simple key‑based idempotency fails when a second request carries different parameters, and demonstrates how to use database row locks, request fingerprinting, state machines, and explicit error handling to guarantee safe, non‑duplicate execution in payment‑critical APIs.
Many developers think idempotency is trivial—just remember a request key and return the previous result for duplicate calls. This works for demos, but in production, especially for payment APIs, the second request may have different payload, may arrive while the first is still processing, or the server may crash, leading to double charges or missed notifications.
Correct idempotency requires storing not only the key but also the meaning of that request. When a request arrives, extract the business‑critical fields (e.g., operation type, account, amount, currency), order them consistently, and compute a hash. Store the hash together with the key. On a retry, compare the new hash with the stored one; if they differ, reject the request with 409 Conflict.
To prevent two concurrent requests from both executing, use an atomic database insert of a row with status “processing”. The database guarantees that only the first insert succeeds; the loser detects the unique‑key violation, reads the existing status, and either waits for completion or returns an appropriate response. This "insert‑first‑then‑process" pattern is far more reliable than a pre‑check‑then‑insert approach.
If the processing state is "processing", the API should not leave the client guessing. Return 202 to indicate the request is being handled, or 409 with a Retry-After header to ask the client to retry later. Returning 500 without context is a bad practice.
External service failures (e.g., a payment gateway confirming success while the server crashes before persisting the result) require a non‑repeating transaction identifier. On retry, query the gateway with that identifier instead of re‑issuing the payment. This separates "query status" from "repeat operation".
Message queues also suffer from at‑least‑once delivery. Before processing a message, insert a record with a unique key (message ID or business ID). If the insert fails due to a duplicate key, skip processing. This protects downstream actions such as sending emails or updating ledgers.
Idempotent records must expire. Define a TTL (e.g., 24 hours) after which the key is treated as new. Document the behavior for expired keys, and ensure that "processing" records that outlive their TTL are reconciled rather than simply deleted.
Not every operation needs heavyweight idempotency. Simple updates that are inherently idempotent (e.g., changing a username) can rely on a unique constraint. Reserve the full mechanism for actions where duplicate execution causes financial loss, inventory depletion, or irreversible side effects.
Before shipping, run practical checks: concurrent request test, parameter‑tampering test (same key, different amount), downstream timeout/crash test, duplicate message test, and expiration replay test. Passing these validates that the system truly implements robust idempotency rather than a superficial cache.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
