DEV Community

desgh white
desgh white

Posted on

Idempotent Promo-Code Redemption: Making 'Apply Once' Actually Mean Once

Promo codes look like a string lookup and turn into a concurrency nightmare the first time a user double-taps "apply" on a flaky connection. Here is how to make redemption exactly-once instead of best-effort.

The bug you will ship first

The naive flow reads the code, checks it is unused, then marks it used. Two requests interleave between the check and the write, and the same code redeems twice:

SELECT used FROM codes WHERE code = $1;   -- both see used = false
UPDATE codes SET used = true WHERE code = $1;  -- both succeed
Enter fullscreen mode Exit fullscreen mode

Make the write the check

Collapse read-then-write into a single conditional update and trust the row count, not a prior read:

UPDATE codes SET used = true, redeemed_by = $2
WHERE code = $1 AND used = false;
-- rows_affected == 1  -> this request won the redemption
-- rows_affected == 0  -> already used, reject
Enter fullscreen mode Exit fullscreen mode

The database's row lock is your mutex. No application-level coordination, no race.

Idempotency keys for the retry storm

Mobile clients retry. Attach a client-generated idempotency key to the redeem call and store it with the result, so a retried request returns the original outcome instead of a second attempt:

POST /redeem  { "code": "WELCOME", "idempotency_key": "uuid" }
Enter fullscreen mode Exit fullscreen mode

Reference

Operators that run bonus codes at scale are a decent study in how redemption, eligibility windows and per-user caps fit together. Voxcasino's oficjalna strona lays out how a single code maps to concrete terms, which is a useful reference for the states worth modeling before you write the update.

Takeaway

Push the uniqueness check into a conditional UPDATE, key retries with an idempotency token, and let the row count tell you who won. "Apply once" becomes a guarantee instead of a hope.

Top comments (0)