· 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 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:

  1. Read the seat row, capturing its current version number and status.
  2. 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'.
  3. 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:

  1. 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.
  2. Perform the multi-step booking logic (price calc, promo validation) while holding the locks.
  3. Commit the actual seat status changes to the database.
  4. 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:

  1. Incoming HoldSeat requests are placed on a message queue (Kafka, SQS, or similar) rather than hitting the database directly.
  2. A pool of workers consumes the queue at a controlled rate, applying the optimistic-locking logic per message.
  3. 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

MechanismSolvesLatency costFailure mode if misused
Optimistic locking (CAS)Race condition on a single seatLow (retry loop on conflict)High conflict rate under extreme contention if not paired with queueing
Distributed lock (Redis/Redlock)Multi-step booking coordinationLow, if TTL is shortSeat stuck if lock never expires (missing TTL)
Event sourcingAuditability, dispute resolution, replayable stateSlightly higher write cost (append event + update projection)Event log growth without archiving/snapshotting
CQRSRead/write contention separationRead-side eventual consistency lag (ms to low seconds)Stale seat map shown to users if projection lag grows too large
Queue-based processingWrite path overload during traffic spikesAdded queueing delay under loadQueue backlog if consumers can’t keep pace with producers

Full Booking Request Data Flow

  1. 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).
  2. Request enters the queue (possibly after passing a virtual waiting room rate limiter if the event is hot).
  3. A worker consumes the message, acquires a short-lived distributed lock on seat S (and any other seats in the same group booking).
  4. Worker performs optimistic-locking CAS against the seats table: availableheld, with an expiration timestamp.
  5. Worker appends a SeatHeld event to the event log and releases the distributed lock.
  6. CQRS read-side consumer picks up the SeatHeld event asynchronously and updates the seat map projection, so other browsing users soon see the seat as unavailable.
  7. User completes payment within the hold window; a ConfirmPayment command follows the same queue → lock → write → event pattern, appending SeatSold.
  8. If the hold expires first, a scheduled sweep (or a delayed message re-enqueued at hold-creation time) fires a ReleaseSeat command, transitioning the seat back to available and appending a HoldExpired event, which the waitlist consumer can also react to.

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.

Back to Blog

Related Posts

View All Posts »