How a gift card gets issued
Every gift card starts the same way: somebody pays for one, and the business now owes that amount back. Get the issue step wrong and everything downstream is built on sand — a duplicate code, a balance stored as a rounded dollar figure, or a card that exists in the till but not in the ledger. This post walks through how a card gets issued: a unique code that is awkward to guess, an opening balance held in integer cents, a conditional write that refuses to overwrite an existing card, and the first row appended to an immutable ledger.
Key takeaways
- A card code is a bearer instrument, so it’s minted from a cryptographic random source and formatted to be awkward to guess and awkward to mistype.
- The opening balance is set once, in integer cents, from the amount actually paid — never a rounded dollar figure.
- The card is written with a strict “only if this code does not already exist” condition, so a code collision can never overwrite a live card.
- Issuing the card and appending its opening ledger row happen as one DynamoDB transaction — both land, or neither does.
- A retry of the same issue request is idempotent: it returns the existing card rather than minting a second one or charging twice.
Minting a code that resists guessing
The moment a card is sold, its code becomes money. Anyone who holds the code can spend the balance, so the first job of issuing is to mint a code that is hard to guess from another one and hard to stumble onto at random. A sequential number — card 1001, card 1002 — is the worst case: knowing one card tells you a hundred others. Instead the issue step pulls bytes from a cryptographic random source and encodes them into a code like GC-7K2M-4QYX: a fixed prefix so staff recognise it, then two groups drawn from an alphabet that drops the easily-confused characters (no 0/O, no 1/I/l) so it survives being read aloud or typed off a printed slip.
The code space is large enough that guessing is hopeless — far more combinations than a business will ever issue — so an attacker cannot simply try codes until one has a balance. That property matters again in part 4, where the fraud caps assume a leaked code is the realistic threat, not a guessed one.
Setting the balance in cents, once
The opening balance is whatever the customer actually paid, stored as an integer number of cents. A $50 card is 5000, a $25 card is 2500, a $37.50 promotional card is 3750. There is no floating-point anywhere near a balance: 0.1 + 0.2 famously is not 0.3 in a float, and a ledger that holds other people’s money cannot afford that drift accumulating over thousands of redemptions. Every figure in the system — balances, spends, caps, breakage — is an integer count of cents, and only ever formatted as dollars for display on a receipt.
attribute_not_exists(code). A repeat request is idempotent.Writing the card without clobbering one
Two writes have to land together: the card itself in the cards table, and its opening row in the ledger. The issue step does them as a single DynamoDB transaction, so either both commit or neither does — there is no state where a card exists with no ledger history, or a ledger row points at a card that was never written.
The card write carries a condition: attribute_not_exists(code). It means “create this card only if no card with this code already exists.” The code space is enormous, so a genuine collision is astronomically unlikely — but “astronomically unlikely” is not “impossible”, and a system holding money should never silently overwrite a live balance. If the condition fails, the issue step simply mints a fresh code and tries again, and nobody’s balance is touched.
Making a retry safe
Tills lose connectivity mid-sale. A cashier taps “sell”, the network drops before the response gets back, and the natural reaction is to tap it again. Without care that mints two cards and, worse, could charge the customer twice. The issue step is therefore idempotent: every sell request carries a client-supplied request id, and the step records that id against the card it created. A repeat with the same id returns the card that already exists — same code, same balance — rather than minting a second one. The till sees one card, the customer is charged once, and the ledger has exactly one opening row.
Design rules for issuing
- Codes come from a cryptographic random source, not a sequence, and skip easily-confused characters.
- The opening balance is integer cents taken from the amount actually paid.
- The card row and its ledger row are written in one transaction — both or neither.
- A strict
attribute_not_exists(code)condition means a code can never overwrite a live card. - A repeat sell request is idempotent by request id: one card, one charge, one opening row.