· SWE Editorial · System Design · 6 min read
Design a Ticketing System: Architecture and Data Flow
A deep dive into ticketing system internals: optimistic locking, distributed locks, event sourcing, CQRS, and queue-based processing, with a full booking-request walkthrough.
A ticketing system that survives a Taylor Swift-scale on-sale isn’t built around a simple CRUD API — it’s built around careful concurrency control, an event-driven architecture, and a read/write path that are deliberately separated. This piece traces the full architecture and data flow, going deeper than the interview-answer framing into how these systems actually work in production.
The Central Design Tension
Every ticketing system architecture is a response to one tension: booking a seat is a low-latency, high-contention write, while browsing available seats is a high-volume, latency-tolerant read. Trying to serve both from the same synchronous path is what causes ticketing sites to fall over during popular on-sales. The fix is separating these paths architecturally.
Optimistic Locking in Detail
When a user attempts to hold seat S, the write path does the following:
- Read the seat row, capturing its current
versionnumber andstatus. - Attempt
UPDATE seats SET status = 'held', held_by = :user, expires_at = :now+300s, version = version + 1 WHERE seat_id = :S AND version = :read_version AND status = 'available'. - If the update affects 1 row, the hold succeeded. If it affects 0 rows, another request won the race — the version or status changed between read and write — and the client is told to pick another seat or retry.
This pattern (often called compare-and-swap or CAS) avoids holding a database lock for the duration of the request, which matters enormously under contention: a lock held even 50ms longer than necessary during a hot on-sale can cascade into serious queueing.
Distributed Locks for Multi-Step Bookings
Real bookings are rarely a single atomic update — they often involve holding multiple seats together (a group booking), applying a promo code, and calculating dynamic pricing, all of which shouldn’t happen inside one long database transaction. This is where a distributed lock (backed by Redis, using something like the Redlock algorithm, or a simpler single-instance lock with a TTL) comes in:
- Acquire a lock keyed on
seat:S1,seat:S2, … for all seats in the group, with a short TTL (e.g., 3-5 seconds) as a safety net. - Perform the multi-step booking logic (price calc, promo validation) while holding the locks.
- Commit the actual seat status changes to the database.
- Release the locks (or let them expire, as a fail-safe if the process crashes mid-flow).
The TTL is critical: if the process crashes while holding the lock, you don’t want the seat permanently stuck — the lock must self-expire even without an explicit release.
Event Sourcing for the Booking Ledger
Rather than only storing current seat state, many ticketing architectures append every state transition as an immutable event: SeatHeld, SeatReleased, SeatSold, HoldExpired, PaymentFailed. This event log becomes the source of truth, and the current seat status is a derived projection built by replaying events.
Why this matters for a ticketing system specifically: disputes and refunds are common, and being able to answer “what exactly happened to this seat, in order, with timestamps” is operationally invaluable — far more useful than a single mutable status column that only shows the current state. It also naturally supports rebuilding read models (see CQRS below) and auditing for fraud (e.g., detecting a user rapidly holding and releasing many seats, a bot-like pattern).
CQRS: Separating Reads from Writes
Command Query Responsibility Segregation (CQRS) formalizes the split hinted at earlier:
- Write side (commands):
HoldSeat,ConfirmPayment,ReleaseSeat. These go through the optimistic-locking/distributed-lock path described above and append events to the log. - Read side (queries): A denormalized, eventually-consistent view of seat availability, built by consuming the event stream and updating a fast read store (e.g., an in-memory cache or a read-optimized database table) that serves the actual seat map UI.
The benefit: the read path (which is by far the highest-volume traffic — everyone browsing seats) never contends with the write path’s locks at all. It’s just reading a projection that’s updated asynchronously, typically with latency in the tens to low hundreds of milliseconds — imperceptible to users but architecturally decoupled from the write hot path.
Queue-Based Processing for the Write Path
During a hot on-sale, incoming hold requests can exceed what the database can safely process concurrently. The standard mitigation is a queue in front of the write path:
- Incoming
HoldSeatrequests are placed on a message queue (Kafka, SQS, or similar) rather than hitting the database directly. - A pool of workers consumes the queue at a controlled rate, applying the optimistic-locking logic per message.
- This smooths out traffic spikes — instead of the database seeing 100,000 concurrent write attempts in one second, it processes them at a sustainable rate, with users experiencing a short queueing delay rather than the system falling over.
This is also where a virtual waiting room attaches architecturally: it’s effectively a rate limiter placed even earlier, before requests are allowed to enter the queue at all, protecting the entire downstream system from an unbounded traffic spike.
Comparison Table
| Mechanism | Solves | Latency cost | Failure mode if misused |
|---|---|---|---|
| Optimistic locking (CAS) | Race condition on a single seat | Low (retry loop on conflict) | High conflict rate under extreme contention if not paired with queueing |
| Distributed lock (Redis/Redlock) | Multi-step booking coordination | Low, if TTL is short | Seat stuck if lock never expires (missing TTL) |
| Event sourcing | Auditability, dispute resolution, replayable state | Slightly higher write cost (append event + update projection) | Event log growth without archiving/snapshotting |
| CQRS | Read/write contention separation | Read-side eventual consistency lag (ms to low seconds) | Stale seat map shown to users if projection lag grows too large |
| Queue-based processing | Write path overload during traffic spikes | Added queueing delay under load | Queue backlog if consumers can’t keep pace with producers |
Full Booking Request Data Flow
- User clicks “Reserve” on seat S in the UI, which was rendered from the CQRS read-side projection (already slightly stale, but fine for browsing).
- Request enters the queue (possibly after passing a virtual waiting room rate limiter if the event is hot).
- A worker consumes the message, acquires a short-lived distributed lock on seat S (and any other seats in the same group booking).
- Worker performs optimistic-locking CAS against the seats table:
available→held, with an expiration timestamp. - Worker appends a
SeatHeldevent to the event log and releases the distributed lock. - CQRS read-side consumer picks up the
SeatHeldevent asynchronously and updates the seat map projection, so other browsing users soon see the seat as unavailable. - User completes payment within the hold window; a
ConfirmPaymentcommand follows the same queue → lock → write → event pattern, appendingSeatSold. - If the hold expires first, a scheduled sweep (or a delayed message re-enqueued at hold-creation time) fires a
ReleaseSeatcommand, transitioning the seat back toavailableand appending aHoldExpiredevent, which the waitlist consumer can also react to.
Related Reading
For the interview-answer framing of this same problem — how to structure your response, what to clarify up front, and how to handle waitlists — see Design a Ticketing System: System Design Interview Guide.
Key Takeaways
- Optimistic locking handles single-seat races cheaply; distributed locks coordinate multi-step, multi-seat bookings.
- Event sourcing turns the booking history into an auditable, replayable ledger — valuable for disputes and fraud detection.
- CQRS decouples the high-volume read path (seat browsing) from the low-volume, high-contention write path (seat booking).
- Queue-based processing in front of the write path is what actually survives a traffic spike 100x normal demand.
Go Deeper
The 0-to-1 SWE Interview Playbook (Amazon: https://www.amazon.com/dp/B0H256Z1MF?tag=sirjohnnymai-20) walks through architecture-level breakdowns like this one across concurrency-heavy systems, useful for both interview prep and real-world design reviews.