Back to work
case study 03

Encore

Ticketing where the last seat can never sell twice.

Demo: organizer@encore.demo, password Passw0rd!

Role
Solo. Design, backend, frontend, deploy
Type
Event ticketing platform
Stack
.NET 10, Blazor WebAssembly, PostgreSQL
Proof
40 buyers, 15 seats, none oversold

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

01

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()
02

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"
03

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.

From the repobackend/Encore.Api/Services/TicketPurchaseService.cs
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

Aubergine#241021
Paper#F7EFE1
Magenta#EC3F8F
Amber#F2A93B

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.

Proof, not promises

Race tested40 buyers hit the last 15 seats at once. None oversold
Real PostgresIntegration tests run the real API against Postgres
Full buy flowSign-in, hold, payment and QR ticket, tested over HTTP
CI on every pushFormat check, build and tests against Postgres

What I'd build next

  • Door check-in that scans and burns each QR
  • Reserved seating with a seat map
  • A waitlist that gets released seats first

Need a system that holds
under pressure?

Let's work together Next project: Wall it