Encore
Ticketing where the last seat can never sell twice.
Demo: organizer@encore.demo, password Passw0rd!
The problem
A big on-sale is a stampede. Hundreds of people press Buy on the same last seats in the same second. If two of them read "1 left" and both pay, someone shows up with a ticket for a seat that does not exist.
I wanted checkout to stay correct in a stampede, and to prove it with a test instead of a promise.
Three decisions that made it work
Read, check, then race
Every ticket type carries a version number. A purchase only saves if that version is unchanged since it was read. If another buyer got there first, the request retries against fresh numbers.
IsConcurrencyToken() The database has the last word
A check constraint keeps sold between zero and capacity. Even if the app logic had a bug, Postgres would refuse to oversell.
"Sold" <= "Capacity" Holds that let go
Checkout holds seats for 15 minutes. A background sweeper runs every 30 seconds and releases expired holds, so abandoned carts never lock up a show.
if (tt.Sold + quantity > tt.Capacity)
return PurchaseResult.SoldOut(tt.Capacity - tt.Sold);
var now = DateTimeOffset.UtcNow;
tt.Sold += quantity;
tt.Version += 1; // advance the concurrency token so a racing writer's UPDATE hits 0 rows
...
catch (DbUpdateConcurrencyException)
{
await tx.RollbackAsync(ct);
// A concurrent buyer changed this ticket type between our read and write. Retry.
} The losing writer updates zero rows, rolls back and tries again with fresh numbers. A test fires 40 buyers at the last 15 seats at the same instant: 15 sold, 25 told it is sold out, none oversold.
Designed like a night out
Tickets look like tickets. Paper stock, a perforated stub, and a QR code printed in the brand ink. Buy one and it tears in.
Aubergine and magenta carry the mood of a venue at night. Organizers get a calmer dashboard: sales by ticket type and by day, attendees and a CSV export.