Fundamentals/API Design

Idempotency

API Design

Visual Representation

Rendering diagram...

What is it?

🔘 Think of it like an elevator button:

You press the elevator "up" button. Nothing happens for a few seconds. You press it again... and again... and again. Does the elevator come 4 times? NO! Pressing it 1 time or 100 times has the SAME result — the elevator comes once.

That's idempotency — doing something multiple times produces the SAME result as doing it once.

In APIs: an idempotent operation means if you accidentally send the same request 5 times (due to network retries, user double-clicks, etc.), the result is the same as sending it once. Your bank account isn't charged 5 times!

💡 Simple Summary: Idempotent = safe to retry. Doing it 1 time or 100 times gives the same outcome. Critical for preventing duplicate charges, orders, or messages.

How it works — Like you're watching it happen

The nightmare scenario WITHOUT idempotency:

  • You click "Pay ₹5000" on Swiggy.
  • Your phone sends POST /payments to the server.
  • Network hiccup — you don't get a response. Did it work? 🤔
  • App auto-retries the same POST /payments request.
  • Server receives it AGAIN. Creates ANOTHER payment. You're charged ₹10,000! 💀
  • WITH idempotency (how it should work):

  • You click "Pay ₹5000." App generates an idempotency key: "idem_abc123."
  • Sends POST /payments with header: Idempotency-Key: idem_abc123.
  • Server processes it: charges ₹5000, stores the key + response in database.
  • Network hiccup, no response received.
  • App retries: same POST /payments with SAME Idempotency-Key: idem_abc123.
  • Server sees "I already processed idem_abc123" → returns the STORED response without processing again. No double charge! ✅
  • But wait — aren't GET requests already idempotent? Why is this mainly about POST?

    Exactly! GET, PUT, DELETE are naturally idempotent by design. GET /user/123 returns the same user whether you call it 1 or 100 times. PUT /user/123 sets the user to a specific state regardless of how many times you call it. DELETE /user/123 deletes the user — calling again just returns "already deleted." But POST /orders CREATES a new resource each time — that's why POST needs explicit idempotency protection.

    Why should you care? (Interview perspective)

  • 🎯 "How do you prevent duplicate orders/payments?" — idempotency is THE answer
  • Critical for any financial or transactional system design
  • Shows you understand real-world network unreliability
  • Demonstrates production-readiness thinking (what happens when things go wrong)
  • Key Things to Remember

  • Idempotent HTTP methods — GET (always), PUT (set to state X, same every time), DELETE (delete once or delete again, result = deleted). NOT idempotent: POST (creates new resource each time).
  • Idempotency key — A unique ID (UUID) sent by the client with the request. Server checks: "have I seen this key before?" If yes → return stored response. If no → process and store.
  • Where to store keys — Database table (idempotency_key, response, created_at, expires_at). Or Redis with TTL for faster lookups.
  • Key TTL — Don't store forever! Keep for 24-48 hours (enough for retries), then delete. Prevents unbounded storage growth.
  • Client generates the key — NOT the server. If the server generated it, you couldn't retry (you'd never get the key back from the failed first request).
  • Stripe's implementation — Send Idempotency-Key header with POST requests. Stripe stores the result for 24 hours. Same key = same response, guaranteed no double charges.
  • At-least-once + idempotency = exactly-once — Networks guarantee at-least-once delivery (might deliver duplicates). Add idempotency → effectively get exactly-once processing.
  • Database constraint — Add UNIQUE constraint on idempotency_key column. Even if two retries arrive simultaneously, only one INSERT succeeds. The other gets a duplicate key error and returns the first response.
  • Natural idempotency keys — Sometimes the business provides one: "order_id=ORD_789." Processing order 789 twice should charge once. Use the order_id itself as idempotency key.
  • Idempotency ≠ same response body — GET /time returns different values each time. It's still idempotent because it has no SIDE EFFECTS (doesn't change anything). Idempotency is about side effects, not response content.
  • Real Examples You Use Daily

    💳 Stripe payments — Every POST to /charges includes an Idempotency-Key header. If your server crashes after charging but before recording it, the retry won't double-charge. Stripe literally built their business on this guarantee.

    🛒 Amazon orders — Click "Place Order" twice due to slow network? Amazon's idempotency logic ensures only ONE order is created. They use the cart session + checkout intent as a natural idempotency key.

    💬 WhatsApp messages — Send a message on flaky network. Your phone might retry sending. WhatsApp uses message IDs (client-generated UUID) to ensure the same message isn't delivered twice — even if sent multiple times.

    🚗 Uber ride requests — Tap "Request Ride" in a tunnel with bad signal. Phone retries. Uber ensures you get ONE ride, not three. The ride request has a unique intent ID that prevents duplicates.

    Common Mistakes in Interviews

    Forgetting WHY it matters — "Idempotency means same result multiple times." But WHY? Because networks are unreliable, retries happen, and without it, you get duplicate charges/orders. Always connect to the real-world impact.

    Thinking server generates the key — If the server generated the idempotency key, you'd need to receive the response to get it. But the problem IS that you didn't receive the response! Client MUST generate it before sending.

    Not handling concurrent retries — Two retries arrive at the same millisecond. Without a database UNIQUE constraint, both might process. Use DB uniqueness + check-then-act pattern.

    Storing idempotency keys forever — Unbounded storage growth! Set a TTL (24-48 hours). If someone retries after 48 hours, it's not a retry — it's a new request.

    Calling GET idempotent "because it returns the same thing" — No! GET /random returns different values each time. It's still idempotent because it has NO SIDE EFFECTS. Idempotency is about not changing state, not about returning identical responses.

    🎯 Interview One-Liner

    "I'd ensure idempotency for all mutating operations using a client-generated idempotency key stored server-side with a unique constraint — so network retries and duplicate requests are safely handled without double-processing, critical for payment and order flows."

    Interview Q&A

    Q: How would you implement an idempotent payment API?

    Client generates a UUID idempotency key before the request. Sends POST /payments with Idempotency-Key header. Server logic: (1) Check if key exists in DB → if yes, return stored response. (2) If no, start transaction: insert key with "processing" status, process payment, update key with response, commit. (3) Return response. Concurrent duplicates: DB UNIQUE constraint on key ensures only one processes. The other gets a constraint violation and queries for the stored response.

    Q: What happens if the server crashes DURING processing (after deducting money but before storing the idempotency response)?

    This is the hardest case. Solutions: (1) Use database transactions — payment deduction and idempotency key storage happen in ONE atomic transaction. Both succeed or both fail. (2) Two-phase approach: first mark key as "processing," then process, then mark "complete." On crash recovery: find "processing" keys and check if payment actually went through (reconciliation). (3) In practice, payment gateways (Stripe) handle this by providing their own idempotency.

    Q: Client retries a request after 2 days. Should idempotency key still be honored?

    No — set a reasonable TTL (24-48 hours). After expiration, treat it as a new request. Why? (1) Storage would grow unbounded. (2) After 2 days, context has changed — it's likely an intentional new action, not a retry. (3) Most retries happen within seconds to minutes. Document the TTL in API docs so clients know the guarantee window.

    Q: How do you make DELETE idempotent?

    DELETE /users/123 — first call deletes the user, returns 200. Second call: user already deleted. Instead of returning 404 (which suggests an error), return 200 or 204 (the desired state "user doesn't exist" is achieved). The end state is the same regardless of how many times you call it. Some APIs return 404 on subsequent deletes — technically fine but less "idempotent-feeling" for the client.

    Q: PUT vs POST — why is PUT naturally idempotent?

    PUT /users/123 with body {name: "Prakshay", age: 25} SETS the user to exactly that state. Call it once: user is {name: "Prakshay", age: 25}. Call it 100 times: user is still {name: "Prakshay", age: 25}. Same result. POST /users creates a NEW user each time. Call it 100 times → 100 users created! That's why POST needs explicit idempotency keys.

    Quick Quiz

    1/5

    You click "Pay ₹5000" but your network is flaky. Your app retries the payment request 3 times. Without idempotency, what happens?