DEV Community

Cover image for Why You Should Never Use a Random UUID as an Idempotency Key in Payment APIs
patrick0806
patrick0806

Posted on

Why You Should Never Use a Random UUID as an Idempotency Key in Payment APIs

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:

  1. User tries to subscribe to your service.
  2. You send a request to Mercado Pago β†’ an invoice is created.
  3. User refreshes the page and submits again.
  4. 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
Enter fullscreen mode Exit fullscreen mode

❌ 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 or sessionId 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"
}));
Enter fullscreen mode Exit fullscreen mode

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)