Ticket booking architecture.
A named seat can be sold exactly once, and ten thousand people want it at the same instant. Everything here follows from that.
The components
Every row below is read from the graph that produced the diagram above, so the two cannot disagree.
| Component | Tier | Why it is there |
|---|---|---|
| Web & mobile | Client | Supporting component |
| Waiting room | Edge | Admission control before the sale opens |
| API gateway | Edge | Supporting component |
| Seat inventory | Application | The contended resource, one writer per seat |
| Hold service | Application | TTL lock, released automatically |
| Booking service | Application | Supporting component |
| Payments | Application | Supporting component |
| Expiry worker | Application | Returns abandoned holds to inventory |
| Redis | Data | Holds, with TTL doing the cleanup |
| Postgres | Data | Unique constraint is the real guarantee |
| Kafka | Data | Supporting component |
| Payment provider | External | Supporting component |
Design decisions worth arguing about
A diagram shows what was chosen. It does not show what it cost, and that is usually the part that matters in a review or an interview.
A waiting room, because admission control beats scaling
For a sale where demand exceeds supply by orders of magnitude, no amount of capacity makes everyone succeed; it just lets everyone fail more expensively. A waiting room admits users at a rate the system can actually serve, which keeps the experience predictable and stops the backend collapsing. The cost is a queue people can see, which is a product decision as much as a technical one.
Hold with a TTL, so abandonment self-heals
A seat has to be reserved while payment is attempted or two people will pay for it. Holding it forever means abandoned checkouts permanently remove inventory. A TTL makes expiry automatic rather than dependent on a cleanup process running correctly. The tradeoff is choosing the duration: too short and slow payers lose seats they were paying for, too long and inventory looks sold out when it is not.
The database constraint is the real guarantee
Redis holds are a coordination mechanism, not a correctness one: a network partition or an expiry at the wrong moment can let two bookings through. A unique constraint on the seat in the transactional store is what makes double booking impossible rather than unlikely. It means the second booking fails at commit time, late in the flow, which is worse for that user and correct for the system.
Payment cannot happen inside the lock
Payment involves a third party and takes seconds, so holding a database lock across it serialises the whole sale on external latency. The hold exists precisely so that the lock can be released while payment proceeds. The consequence is a window where the seat is neither free nor sold, and every part of the system has to represent that state honestly rather than rounding it to one or the other.
How it changes with scale
Load is not steady, it is a spike measured in seconds at a known time. Autoscaling cannot react fast enough, so capacity has to be provisioned ahead of the on-sale and the waiting room has to absorb the rest. This makes the system unusual: it is sized for a handful of moments per year rather than for its average.
Where it breaks first
Contention on a popular seat or section. Everyone attempts the same rows, so the same keys are contended, and lock waits stack up until requests time out. Timed-out requests are retried by users, which adds load, and the section that everyone wants becomes the section nobody can buy. Randomising suggested seats within a price band spreads contention and is far cheaper than making the locking faster.
Draw this yourself
Open the Diagram tab and describe the system. The agent emits a semantic graph rather than coordinates, so you can edit the components and the layout re-solves instead of drifting.
When the shape is right, Implement in code turns the canvas into a markdown specification, every component, every relationship and the notes, and starts a real turn in the Code tab with it.
Questions about this design
Why not simply use a database transaction for the whole booking?
Because it would hold a lock across a payment call to an external provider. At that point one slow provider response blocks the seat, and a provider outage blocks the entire sale.
How long should a hold last?
Long enough for a genuine checkout including a payment challenge, typically several minutes, and visibly counted down in the interface so the user understands the deadline rather than discovering it.
What happens if the expiry worker is down?
Nothing immediately, because Redis TTL releases the hold on its own. The worker reconciles inventory state; making expiry depend on the worker rather than the TTL would turn its downtime into permanently lost inventory.
Does a waiting room need to be fair?
It needs to be seen to be fair, which usually means first-come ordering with a visible position. Randomised admission is arguably fairer and is much harder to explain to someone who has been waiting.
More templates
- Food Delivery System DesignThree sides to coordinate and one deadline that nobody controls: the kitchen. Dispatch quality matters more here than any amount of throughput.
- Multiplayer Game Backend DesignLatency is the product. Every architectural decision here is subordinate to keeping the round trip between a player and the simulation short.
- Multi-Tenant SaaS ArchitectureOne database serving every customer, and exactly one bug standing between that and showing one customer another customer's data.
- Data Warehouse ETL Pipeline DesignLand the raw data first and transform it later, because the transformation you want in six months is not the one you would write today.
Last updated