System design template

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.

Download for macOS
v0.1.33 · .dmg · Apple Silicon & Intel
Signed & notarized by Apple · opens without a Gatekeeper warning
sha256 698955a0187bc039f4c74f5d05a9f10fbb27376a45788a0a241d1326b73873c7
Download for Windows instead
$curl -fsSL https://lucidtrain.com/install.sh | sh
Ticket booking system architecture. 12 components across 5 tiers.
Ticket booking system architecture. Rendered by the same ELK layout engine the app runs: the agent emits components, tiers and edges, and the engine places them, so the boxes cannot overlap.

The components

Every row below is read from the graph that produced the diagram above, so the two cannot disagree.

ComponentTierWhy it is there
Web & mobileClientSupporting component
Waiting roomEdgeAdmission control before the sale opens
API gatewayEdgeSupporting component
Seat inventoryApplicationThe contended resource, one writer per seat
Hold serviceApplicationTTL lock, released automatically
Booking serviceApplicationSupporting component
PaymentsApplicationSupporting component
Expiry workerApplicationReturns abandoned holds to inventory
RedisDataHolds, with TTL doing the cleanup
PostgresDataUnique constraint is the real guarantee
KafkaDataSupporting component
Payment providerExternalSupporting 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.

shell
$ Diagram a ticket booking system: an edge waiting room for admission control, seat inventory, a hold service using Redis TTL locks, booking and payment services, an expiry worker returning abandoned holds, and Postgres with a unique constraint as the final guarantee.

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.

FAQ

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