Imagine you have a saas and your user clicks "Pay", but the network times out, so they hit the button again.
Your backend processes the same request twice.
π₯ Result? Two charges. Angry user. Support ticket. Refund dance.
Recently, I started building a personal project using a monolithic architecture and integrated Stripe for billing. As expected, YouTube began recommending all sorts of payment integration videos.
One of them caught my attention β and nearly made me choke on my dinner.
In the video, the creator was integrating with Mercado Pago and explained the use of the X-Idempotency-Key
header by saying:
"Just use
randomUUID()
from Node here. It just needs to be unique."
Seems harmless, right? But that one line can break the whole concept of idempotency especially in payment systems.
π§ What Is Idempotency, Really?
Idempotency ensures that multiple identical requests only have one effect.
If the first request goes through and the second one is retried β whether due to a flaky network, user impatience, or frontend retry logic β the backend should return the same result, not perform the operation again.
β Example:
- User tries to subscribe to your service.
- You send a request to Mercado Pago β an invoice is created.
- User refreshes the page and submits again.
- If the request includes the same idempotency key, the same invoice is returned β no duplicate charge.
Thatβs the power of headers like:
X-Idempotency-Key: invoice-1234-user-5678
β The Problem with UUID.randomUUID()
If you generate a new UUID for each retry, the server can't detect that it's the same operation.
Each request looks completely unique.
So instead of being safe and idempotent, you get:
- β First request β Charge created
- β Second request β Another charge created
- π£ Third request β Yep... you get the idea
β What You Should Do Instead
An idempotency key must be deterministic:
the same operation should always produce the same key.
You can use:
- A combination of
userId + invoiceId
- A
traceId
orsessionId
from the frontend - A fingerprint of the payload (e.g. hash of the JSON body)
Examples:
const key = "invoice-" + userId + "-" + invoiceId;
or something like
const key = sha256(JSON.stringify({
userId: "1234",
plan: "pro",
price: "49.90"
}));
This ensures idempotency across retries, across network hops, even across frontend reloads.
π‘οΈ Backend Handling
On the server side, the logic is straightforward:
Receive the X-Idempotency-Key header
Check if that key already exists in storage
If yes β return the saved response
If no β process, store the response + key
Storage options:
Relational DB with unique constraint on the key
Redis with TTL
Distributed cache or NoSQL with atomic writes
Bonus tip: log the idempotency key with your trace logs to simplify debugging.
π‘ Real-World Examples: Stripe & Mercado Pago
Both Stripe and Mercado Pago rely heavily on idempotency keys to avoid duplicate charges.
Their APIs require that you set this key once per operation, and they cache the result server-side.
If you change the key on each retry β like using a UUID.randomUUID() β you lose all protection.
π§΅ Pro Tip: Where to Generate the Key?
Ideally, the client(frontend, api caller) should generate the key once per operation and persist it until it's confirmed successful.
If you use traceId or a sessionId, make sure it persists across retries (not regenerated).
On mobile apps, you can store it in local storage or memory.
The important thing is: same intent β same key.
π Final Thoughts
Idempotency isn't about "not crashing on retry" β
it's about not duplicating irreversible effects.
Random UUIDs are great for correlation IDs or primary keys β
but not for identifying business operations.
So next time you're building a billing integration or an API that creates side effects, ask yourself:
βWhat happens if this request is sent twice?β
And remember:
𧨠Random UUIDs β Idempotency
π¬ Have you ever seen this bug happen in production?
Or used UUID.randomUUID() without thinking twice?
Drop a comment below β letβs share some war stories π
Top comments (0)