Your validation does not enforce a promo code limit. The write does.
Read the usage counts, check the rules against them, then increment, and you have a window between the check and the write. Two concurrent checkouts both read "9 | 10 used", both pass validation, both increment. Eleven redemptions on a ten-use code. The rule was correct every single time it ran.
So in our open-source promocodes library the store's record call is a compare-and-set. It takes the usage counts the decision was made against, and it returns nothing at all if those counts moved underneath it. No exception, no partial write, just a signal that the decision is stale.
The redeem path then loops, up to three attempts by default. Every pass re-reads usage and re-runs validation from scratch. It is not a retry of the write but a retry of the whole decision, because the right answer on the second pass may be "rejected" rather than "try again". After the attempts are exhausted it raises a conflict error, deliberately a runtime error and not a rejection. Contention is an infrastructure outcome. A customer being out of uses is a business outcome. Collapse them into one error type and you end up telling a user their valid code is invalid because two people clicked at once.
Two more pieces of the same design.
A known idempotency key returns a reservation marked as replayed and increments nothing. The retry of a request is not a second redemption.
And the read and the write are split on purpose. Quoting validates and prices without spending anything, while capture re-runs every rule through redeem at payment time, optionally against a total that changed since the quote. If a rule now refuses, the rejection is re-raised as a capture failure carrying the stale quote, and nothing is recorded. A refused capture must leave the counters exactly where they were.
The honest limitation: the in-memory store holds the rule with a thread lock. That is correct for one process and no substitute for a database. The compare-and-set contract exists so that a real backing store can honour it with a conditional update, and the calling code does not change.
If you handle promo codes, discounts, or any budget shared across concurrent requests, it is worth knowing which layer does the enforcing, the validator or the write.