This is the five-step framework from chapter one, run in miniature against one real piece of Fanline: every completed order needs a short code a fan can read off their phone at will-call, or a tired staffer can type by hand at eleven at night when the wifi drops and the scanner app won't load.
Functional: given a finished order, produce a short code; given a code plus the buyer's last name as a lightweight second check, look the order back up. Non-functional: the code has to be genuinely unique, short enough to read off a screen or type under bad lighting, and — this is where the problem stops resembling a generic unique-ID exercise — it must never let anyone guess or enumerate somebody else's code, because a code is effectively a bearer token for another person's tickets. For this walkthrough, assume 6 million tickets sell a month on average, and each code gets looked up roughly four times over its life: the purchase confirmation, a wallet-app add, a day-of scan, and the occasional support call.
6,000,000 writes a month works out to about 2.3 a second averaged over the whole month — small. That average hides Fanline's real shape almost completely, though: ticket sales aren't smooth across thirty days, they're brutally concentrated into the first few minutes an on-sale actually opens. A stadium show moving 50,000 tickets in its opening ten minutes is generating roughly 83 confirmation-code writes a second, sustained, for that entire stretch — twenty to thirty times the monthly average, and that's before counting every reserve-then-abandon attempt from fans who lost the race, which run several times higher again. Size for that burst, not the smoothed monthly figure, or the design looks fine on a spreadsheet and falls over the first time it matters. Storage is comfortably not the bottleneck here: each stored record (code, order ID, status, timestamps) runs roughly 250 bytes, so 6M records a month is about 1.5GB, or under 60GB after three full years.
Customer -> Load Balancer -> App Servers (stateless) -> Cache -> Order/Inventory Database
(cache-aside, per the caching chapter)
A successful checkout mints a code, stores {code -> order_id}, and hands the code back with the confirmation. A lookup (GET /orders/{code}) reads the order and, on a match, returns the ticket. Given how read-heavy this is against how rarely a code is actually minted — four lookups per code, and every one of them ideally instant even over spotty venue wifi — a cache-aside layer in front of the lookup path earns its keep even though writes here are comparatively rare.
This is the genuinely interesting piece, and it splits into two goals that actively pull against each other.
Take uniqueness on its own first. A counter-based scheme — one globally increasing number, encoded straight into the code — is unique by construction, so there's nothing to collide and nothing to handle. The risk is that a single shared counter turns into a bottleneck the moment 50,000 tickets sell in ten minutes, so Fanline hands each app server its own pre-allocated block ahead of time instead — server one owns codes 1 through 10,000, server two owns 10,001 through 20,000, and so on — which means no checkout ever has to phone home to a shared service in the middle of a rush just to get its next number.
That settles uniqueness cleanly — and opens a second problem a plain unique-ID scheme never has to think about. A steadily increasing counter is, by its nature, predictable. If code 7F3K9QRT maps to order 40,231, code 7F3K9QRU is a very good guess for order 40,232 — meaning anyone who's bought a single ticket can start guessing at the orders placed right around theirs, and work their way toward enumerating other customers' tickets outright. Guess a code on a URL shortener and you've leaked a link nobody meant to share. Guess one here and you've handed a stranger somebody else's seats.
The fix keeps the counter for uniqueness but never lets its raw value reach a screen: run it through a reversible scramble — a fixed-width, Feistel-style permutation, or any similarly simple invertible mapping — before encoding it, so consecutive counter values turn into codes that look nothing alike and give an attacker no signal about what to try next:
function generateCode(counterValue):
scrambled = reversiblePermute(counterValue) # the counter stays sequential; the output doesn't
return base32Encode(scrambled, alphabet="23456789ABCDEFGHJKMNPQRSTVWXYZ")
# ambiguous glyphs (0/O, 1/I/L) are deliberately left out — a staffer
# squinting at a phone screen at 11pm shouldn't have to guess which is which
The counter still guarantees uniqueness underneath; the scramble is what keeps its actual value from ever becoming public.
The permutation step adds a small, fixed amount of CPU work to every code minted — nothing next to everything else checkout is already doing, so it's not a real cost. The genuine risk is operational: losing the mapping between a scrambled code and its underlying counter (a bug in the permutation, a bad deploy) would be far messier to recover from than a plain sequential ID ever would be, so this exact piece of logic carries disproportionately heavy test coverage for its size. And trimming the alphabet to drop ambiguous glyphs shrinks the usable character space from a full base-32 set to something smaller — which barely registers at Fanline's actual volume, but is exactly the kind of trade worth stating out loud rather than treating as free.