03 · BACKEND · CONCURRENCY · SYSTEM DESIGN
Ticket Booking System
Repository codename · High-Concurrency Flash Sale Engine
A flash-sale booking API built to survive hundreds of buyers hitting the last ticket in the same millisecond — with double-booking ruled out at the database level rather than hoped away in application code.
01 — THE PROBLEM
A flash sale breaks a normal CRUD application in two distinct ways, and they need two distinct fixes.
The database CPU crash. Ten thousand users refreshing a page all ask the same question — how many tickets are left? Answering it from Postgres every single time is what actually takes the system down, and most of those requests are for an event that has already sold out.
The double-booking race. If a hundred users press Buy on the final ticket at the same instant, a standard read-then-write application sells that ticket a hundred times. One customer is served and ninety-nine are owed an apology.
02 — ARCHITECTURE
Request → [ Redis availability check ] → reject in ms if sold out
↓ passes
[ Postgres SELECT ... FOR UPDATE ] → row locked atomically
↓
decrement → commit → booking confirmed03 — BUILD LOG
The decisions that shaped the system, and why each one was made.
-
Layer one — Redis as the traffic shield
Live availability is held in Redis. An incoming request checks the cache first, and a request for a sold-out event is rejected in milliseconds without Postgres ever being consulted. This is what keeps the primary database alive under a spike — it never sees the overwhelming majority of doomed traffic.
-
Layer two — pessimistic row-level locking
The cache protects availability; it cannot protect correctness. Checkout therefore runs
SELECT ... FOR UPDATEinside the transaction. The specific event row is locked atomically, overlapping transactions queue behind it and resolve sequentially at microsecond scale — which makes zero double-bookings a property of the schema rather than a hope about timing. -
Pessimistic over optimistic, deliberately
Optimistic locking retries on conflict. Under a flash sale, conflict is the steady state, so optimistic retries turn into a thundering herd. Pessimistic locking queues the contention instead of amplifying it — the right trade when writes to a single row are the entire workload.
-
Async the whole way down
FastAPI with
asyncpg, so a request waiting on the database frees its worker instead of blocking it. Holding thousands of open connections is the baseline requirement here, not an optimisation. -
Postgres chosen for ACID, not familiarity
The entire correctness argument rests on transactional guarantees. A datastore with relaxed consistency would have relocated the race condition rather than removed it.
-
Proved with load, not assertions
A Locust scenario spins up 500 concurrent workers against the running API. The claim being made is that inventory never goes negative and never oversells — so the test harness exists specifically to try to break that. Under the full spike the system sold exactly 100 tickets: zero oversells, zero database crashes, and no dropped connections.
04 — WALKTHROUGH
Running it end to end.
-
Start the infrastructure
Postgres and Redis come up as containers.
docker-compose up -d -
Run the API
The schema initialises automatically on startup.
python -m venv venv source venv/bin/activate # Windows: .\venv\Scripts\activate pip install -r requirements.txt uvicorn main:app --host 127.0.0.1 --port 8000 -
Watch it live
The bundled UI at the root URL shows ticket sales updating in real time as the load test runs — the most direct way to watch both layers doing their jobs.
http://127.0.0.1:8000/ -
Simulate the flash sale
500 users, ramping at 100 per second, for ten seconds.
locust -f load_test.py --headless -u 500 -r 100 \ --run-time 10s --host http://127.0.0.1:8000
05 — STACK