Part 1 of 7 · Gift card ledger series ~9 min read

A gift card ledger on AWS for a few dollars a month

A small business that sells gift cards is holding other people’s money. A $50 card sold today is a $50 promise to honour later, and the moment two tills both think a card still has $50 on it, that promise quietly breaks. Spreadsheets drift, balances get spent twice, a leaked code gets drained in an afternoon, and nobody can say at month-end how much the business actually owes in unredeemed cards. This post walks through the design of a small ledger that issues a card, redeems against it atomically so a balance can never be spent twice, caps abuse, retires dormant cards on a schedule, and reconciles every cent at month-end.

Key takeaways

  • A gift card is a promise to honour money later, so the ledger holds every balance in integer cents — never a rounded dollar figure, never a float.
  • Three things create activity: issuing a card, redeeming against it, and the scheduled jobs that expire dormant cards and reconcile the books.
  • The centrepiece is redemption: one atomic DynamoDB conditional write that makes a balance impossible to spend twice and impossible to push negative.
  • Velocity and value caps sit on the same write, so a leaked code freezes instead of draining; an immutable ledger records every move for audit.
  • Designed on AWS for about $2.10/month at small-business volume. No model ever touches a balance — the arithmetic is plain code.

The whole system on one page

Before any code, here’s the shape of what we’re designing. A gift card looks trivial — a code and a number that goes down — right up until two tills touch the same card at the same second, or a leaked code gets drained over a lunch break, or the owner is asked at year-end how much the business owes in unredeemed cards and the spreadsheet says one thing while reality says another. The whole system exists to make those three problems impossible rather than unlikely.

System architecture: where gift card activity comes from and the three pieces inside AWS At the top, three external boxes in a row. Far left, "Points of sale" — the tills in the shop and the online checkout, where a customer buys a new gift card or pays with an existing one. Centre, "Card policy" — a small settings store holding the opening-value rules, the expiry window (for example 24 months of no activity), and the fraud caps: a per-redemption ceiling, a daily limit per card, and a velocity limit. Far right, "Owner and customer" — the customer who receives an issue receipt or a redemption receipt by email, and the owner who receives the monthly reconciliation. Each connects by an arrow to the AWS account container below. Points of sale send issue and redeem requests in. Card policy grounds every limit. The owner and customer receive receipts and the monthly report out. Inside the dotted AWS account container are three components in a row. On the left, Issue — mints a unique code and an opening balance in integer cents, and writes the card only if that code does not already exist. In the middle, and drawn larger, Redeem — the centrepiece — performs one atomic conditional write that subtracts the spend only if the balance still covers it and no cap is tripped, so a balance can never be spent twice or go negative. On the right, Expire and reconcile — two scheduled jobs that retire dormant cards and tie issued, redeemed, expired, and outstanding liability together each month. Internal arrows flow left to right, and every component appends a row to an immutable ledger drawn beneath them. A note at the bottom reads: no model ever touches a balance — every figure is plain arithmetic over integer cents. Points of sale tills & online checkout Card policy values, expiry, fraud caps Owner & customer where receipts land issue / redeem limits receipts & monthly report AWS account Issue unique code, opening balance Redeem ONE atomic conditional write — never spent twice Expire & reconcile nightly sweep, monthly report Immutable ledger one appended row per issue, redemption, and expiry No model ever touches a balance — every figure is plain arithmetic over integer cents.
Fig 1. Activity comes from the tills and the online checkout; the policy store grounds every limit; receipts and the monthly report go back out. Inside AWS, Issue mints a card, Redeem deducts atomically, and the scheduled jobs expire and reconcile — each appending to one immutable ledger.

What you set up once

  • The card table and the ledger. Two DynamoDB tables. The cards table has one row per gift card: the code, the balance in integer cents, a status (active, expired, or void), the date issued, the date it expires, and the timestamp of the last activity. The ledger table is append-only — one row for every issue, every redemption, and every expiry, never updated or deleted. The cards table gives you the live balance in one fast read; the ledger gives you the full history and the means to prove that balance is right.
  • The card policy. A short settings store — the opening-value rules, the expiry window (a common choice is 24 months of no activity, where local law allows), and the fraud caps: the most any single redemption may take, the most a card may give up in a day, and how many redemptions are allowed in a short window before the card freezes. Changing a cap or the expiry window is a settings edit, not a deploy.
  • Receipts and the report. SES sends the customer an issue receipt when a card is sold and a redemption receipt with the remaining balance when it is spent against. Once a month the owner gets a reconciliation: opening liability, issued, redeemed, expired, and the closing liability the business still owes.

What runs on every card

  • Issue. Mints a unique, hard-to-guess code, sets the opening balance in cents, and writes the card with a condition that refuses to overwrite an existing one. The same step appends the opening row to the ledger. Part 2 takes this apart.
  • Redeem. The centrepiece. A spend is a single atomic conditional write: subtract the amount only if the balance still covers it and no cap is tripped, and write the new balance — all in one indivisible step. If two tills hit the same card at once, exactly one succeeds and the other is told to re-read and retry. Part 3 is entirely about this, and part 4 about the caps riding alongside it.
  • Expire and reconcile. A nightly EventBridge schedule sweeps for cards dormant past the policy window, retires them, and books the leftover as breakage. A monthly schedule re-derives every balance from the ledger and ties the books together. Part 5 covers both.

In plain words

A customer buys a $50 card at the till. Issue mints code GC-7K2M-4QYX, sets the balance to 5000 cents, writes the card, and emails the receipt. Three weeks later the customer spends $18.40 of it. Redeem runs one conditional write — “subtract 1840 only if the balance is still at least 1840” — the balance becomes 3160, and the receipt says “$31.60 remaining.” At the very same instant a second till tries to put through $40 on the same card; its write hits a now-too-low balance, is rejected, re-reads $31.60, and the cashier sees the real figure rather than overspending the card. Nineteen months later, if the card is never touched again, the nightly sweep retires it and books the $31.60 as breakage. None of it depended on a model, and at no point was the balance wrong.

Design rules that shaped every decision

  • Money is integer cents, everywhere. No floats, no rounded dollars, no drift.
  • Redemption is one atomic conditional write. A balance can never be spent twice and can never go negative.
  • Caps ride on the same write as the balance. A flagged card freezes in place rather than leaking.
  • The ledger is append-only. Every move is recorded; the live balance can always be re-derived and proven.
  • No model touches a balance. Bedrock writes one monthly paragraph and nothing else.
  • Caps and the expiry window live in policy, not code. Tightening a limit doesn’t need a deploy.

Why this shape

Most small businesses run gift cards one of two ways: a spreadsheet of codes and balances, or whatever their till software bolts on. The spreadsheet drifts the first time two people edit it, and it has no answer when a code leaks. The till add-on usually handles a single redemption fine but goes quiet on the questions that matter for money you owe — what stops the same card being spent at two tills at once, what happens when a balance is dormant for two years, and what the true outstanding liability is at month-end.

The shape above answers all three by leaning on one database guarantee: an atomic conditional write. Because the read, the check, and the write happen as a single indivisible step, there is no window for a race, so a balance is never spent twice and never goes negative — without locks, queues, or a model in the loop. Everything else hangs off that: the caps live in the same write, the ledger records every move for audit, and the monthly reconciliation re-derives every balance from that ledger to prove the books still tie out to the cent.

The next four posts walk through each piece in turn: how a gift card gets issued, how a balance gets redeemed atomically, how the fraud caps stop abuse, and how a card expires and reconciles. One diagram per post. A cost breakdown and a final engineering reference at the end.

All posts