A digital-goods store can fail in exactly two ways that really matter:
- It sells the same license key to two people. One of them gets an "already redeemed" error, a refund request and a chargeback.
- It takes someone's money and assigns no key at all. That's worse.
At Increddy, a storefront for game and software keys, preventing both is the whole job. Here's the problem as a puzzle. Try solving it before you scroll.
The puzzle
- A customer pays. Your backend receives
order.placed. - Your payment provider sends
payment.captured. - Either event can arrive more than once, because providers retry by design.
- They don't always arrive in the order you expect.
- Each order must end up with exactly one key. Not zero. Not two.
What's your design?
The answer most people reach first
"Reserve a key when order.placed fires."
It works in every test you'll write. Then one busy weekend a webhook retries, the handler runs twice, and a customer gets two keys. Or the event never shows up, and they get nothing.
Retries aren't an edge case. They're the normal case. A system that only works when every webhook arrives exactly once works in staging and breaks in production.
The second answer (still wrong)
"Fine, check whether the order already has a key before reserving one."
if (!order.hasKey) {
assignKey(order) // two handlers can both get here
}
This is the bug that gets past code review. Two handlers can both run the check, both see "no key", and both write. Check-then-write has a race right in the gap between the check and the write.
What we actually built
One: make the write itself idempotent. Idempotency lives at the data layer, not in an if statement. A second attempt to reserve a key for an order that already has one is a no-op, however many handlers race to do it.
Two: two independent triggers. order.placed is the primary path. payment.captured is a fully independent safety net. Since the write is idempotent, both can fire, both can retry, and the result is still exactly one key.
Three: a sweep for whatever slips through. Some cases fall through both triggers, like a captured payment where no order event ever fires. So a job runs every 15 minutes, finds payments captured more than 5 minutes ago with no key assigned, and fixes them.
That job isn't a patch. Every event-driven system has a tail of cases that need a sweep, not a trigger. It's cheaper to build that in on day one than to find out you need it from a support ticket.
The decision that surprises everyone
There's one more piece, and it runs against every inventory instinct you have: Increddy lets customers buy products that are sold out.
It's on purpose, and why it's the right call for a key store is in the full write-up on our blog.
The rule to steal
Make the state transition idempotent ("this order has a key"), not the event that triggers it. Treat reconciliation as part of the design, not a fix after the first incident.
Want the whole picture? The key engine was one module in a full backend rebuild under a live storefront. Read the Increddy case study on kryeo.com →
We're Kryeo. We build order-fulfillment logic like this as part of our Medusa development work.
Top comments (0)