Case study · Backend systems
Gift cards that can't
be spent twice.
A single-use e-gift-card backend for a hospitality group, where the same code can be presented at any of several independent front desks — and must be honoured exactly once.
The problem
The client wanted trackable gift cards: issued now, monetised later, redeemable at any of their properties. There was no existing system — cards were an idea in a meeting, and most of the product questions were still open, including who would ultimately be allowed to create one.
What made it more than CRUD is that a gift card is a promise about entitlement, redeemable across sites that don't coordinate with each other, carrying real financial value, handled by staff under time pressure. The interesting work was in constraining it, not building it.
Approach
01
Treat the entitlement set as a contract, not configuration
The obvious design is an editable table of card types behind an admin screen. I deliberately didn't build one — entitlements are fixed server-side. An editable set means someone can invent a new one at 2am, and the front desk inherits a card whose meaning no process covers.
02
Push the correctness guarantee down to the database
Redemption is the failure mode with money attached, so it doesn't live in application logic. Rather than read the card, check its state, then write, the whole operation is one conditional write that can only succeed against an unredeemed row.
03
Design the code format around how it actually travels
Codes get read aloud down a phone line and typed by hand at a desk, so they're generated from an alphabet with every visually ambiguous character removed. This is a small decision that only surfaces if you think about the humans in the loop rather than the entropy budget.
04
Split the roles that create value from the roles that approve it
Cards issued for free are functionally a way to mint money. Letting one role both create and finalise a card is an internal-fraud shape, so issuance and approval sit with different roles.
05
Isolate the unresolved decisions so they couldn't block shipping
Two things were genuinely undecided: who creates cards, and whether email delivery would be in scope. Both were designed as edges rather than assumptions — issuance doesn't care what kind of client called it, and delivery is optional, so a card is always created and always shareable even when mail isn't configured.
06
Match the sibling service's conventions, share none of its runtime
The group already ran a related backend maintained by the same small team. This one mirrors its structure, auth model and operational patterns closely, while running as an independent process against its own database.
How it fits together
What shipped
A production API covering the full lifecycle — staff authentication, issuance with an approval step, a public card-view endpoint powering the shareable link, search and filtering for front-desk staff, and single-use redemption with a complete audit trail of who issued a card, who redeemed it, and when.
What I'd do differently
Rate limiting is held in process memory. That's correct for a single instance and quietly wrong the moment this scales horizontally, since each instance would keep its own counters. It was the right trade for shipping, and it's the first thing I'd move to a shared store.