Design Ticketmaster — view, search, and book without double-booking

Design Ticketmaster

Junior-friendly Ticketmaster walkthrough: view/search/book events, no double-booking with Redis seat holds, event cache, virtual waiting room, Elasticsearch + CDC, and interview bars by level.

Understanding the problem

Ticketmaster (think BookMyShow for arenas) sells scarce seats under flash-sale load. This interview focuses on view → search → hold → pay without selling the same seat twice — not building the full CMS or pricing engine.

Pick seats, hold for ten minutes while paying, confirm one buyer per ticket.
Hold the seat while you pay — one ticket, one buyer.
01View

Event + seat map.

02Search

<500ms finds.

03Book

Hold then pay.

04Scale

Cache + waiting room.

Pair with Delivery framework, Contention, Scaling reads, Caching, Elasticsearch, and Real-time updates.

Functional requirements

Non-functional requirements

Core entities

Nouns first — full columns later:

Event, Venue, Performer, Ticket, Booking, and User entities.
Event sits at the center; Ticket is a seat instance; Booking groups tickets under one payment.
  • Event — date, description, type, links to venue + performer.
  • User — buyer.
  • Performer — artist/team/collective.
  • Venue — location, capacity, seatMap (JSON / related table for the UI).
  • Ticket — seat + price + status (available / held / booked); one per seat when an event is created.
  • Booking — user + ticket IDs + payment status (keeps multi-seat orders together).

API

Start simple; evolve booking into reserve + confirm once you hit the checkout UX problem.

GET /events/:eventId -> Event & Venue & Performer & Ticket[]
// tickets drive the interactive seat map

GET /events/search?keyword=&start=&end=&pageSize=&page= -> Event[]

// v1 — evolves later
POST /bookings/:eventId
{ "ticketIds": [...], "paymentDetails": ... } -> bookingId

// v2 after deep dive
POST /bookings/reserve  { ticketId, userId } -> bookingId
POST /bookings/confirm  { bookingId, paymentToken } -> Booking

HLD — view an event

Client hits the event page → gateway → Event Service → Postgres (event + venue + performer + tickets for the seat map).

Client through API Gateway to Event Service and Database with Event Venue Performer schemas.
View Event Flow — auth / rate limit / route at the gateway; Event Service owns the read.
  1. Client GET /events/:eventId.
  2. API Gateway: auth, rate limit, route.
  3. Event Service loads event, venue, performer, tickets → response.

HLD — search events

First cut: a Search Service that filters the events table directly. Good enough to draw; too slow for production — fix in deep dives.

Client Gateway Search Service and Event Service querying the database.
Search Flow — query the event table directly (temporary).

HLD — book tickets

Avoid two people paying for the same seat. Use an ACID store — PostgreSQL — with row locks or OCC. Payments via Stripe (tokenize on client; webhook confirms).

Search Event and Booking services with Stripe and PostgreSQL.
Simple Booking Flow — Booking Service + Postgres transaction + Stripe.

Simple POST /bookings that charges immediately works functionally but feels awful: you fill payment and learn the seat vanished. Deep dive #1 fixes that with a timed hold.

Deep dive — reserve then confirm

Hold the seat while the user pays. If they abandon checkout, release it automatically.

  • Good: Ticket status = held + expires_at; cron / sweeper releases expired holds.
  • Great: Redis ticket lock {ticketId → userId} with TTL ~10 min; Booking row in-progress; Stripe webhook marks ticket sold + booking confirmed (idempotent).
Booking Service with Ticket Lock Redis TTL 10 minutes and Stripe.
Booking Flow — reserve locks in Redis; confirm via Stripe webhook.
  1. User picks a seat → reserve(ticketId, userId).
  2. Booking Service sets Redis lock (TTL 10m) + writes in-progress booking → return bookingId.
  3. Client tokenizes card with Stripe.js; server creates PaymentIntent with bookingId in metadata.
  4. Webhook (idempotent on bookingId): ticket → sold, booking → confirmed; drop lock.
  5. If TTL expires first, seat is free again.

Deep dive — scale the view path

Hot events hammer one page. Cache event details, venue, and seat-map shells aggressively. Browse can be briefly stale; booking always checks the lock + DB.

Event Service with Redis event cache and Booking with ticket lock.
Caching — Event cache (Redis) for views; separate Redis for ticket locks.

Deep dive — virtual waiting room

Millions hitting reserve at once will melt Booking + Redis even with locks. A virtual waiting room admits a controlled concurrency into checkout — better UX than random 5xx, and it protects the write path.

Waiting queue between API Gateway and Booking Service.
Virtual Waiting Room — queue before Booking Service during drops.

Seat-map freshness is related: polling / short TTL is usually enough; full SSE for every seat click is overkill unless the interviewer pushes real-time. See Real-time updates.

Deep dive — fast search

LIKE '%Taylor%' is a table scan. Move search to Elasticsearch (index name, description, venue, performer, date). Keep Postgres as source of truth; sync via CDC. Enable node query cache for repeated searches.

-- slow starting point
SELECT * FROM Events
WHERE name LIKE '%Taylor%'
   OR description LIKE '%Taylor%';
Search Service to Elasticsearch with CDC from PostgreSQL.
Search Service → Elasticsearch; Postgres → ES via CDC.

Final design

Final design with CDN, gateway, Search Event Booking, Elasticsearch, Postgres, Redis, Stripe.
Final Design — CDN → Gateway → Search / Event / Booking (+ waiting queue) → ES + Postgres + Redis locks + Stripe.
  • CDN — static assets + cacheable event shells.
  • Search Service — Elasticsearch (+ query cache).
  • Event Service — Redis event cache → Postgres.
  • Booking Service — waiting room → Redis TTL locks → Postgres ACID → Stripe webhooks.

Cost and performance levers

What interviewers expect by level

Architecture by level

Three progressive sketches — beginner spine, mid optimizations, pro production depth. Use the matching diagram in the interview; say the tradeoff out loud.

Beginner Ticketmaster: Event, Search, Booking with Postgres and Stripe.
Beginner architecture

Pros: Clear FR coverage — view, search, book — with ACID booking + Stripe for payments. Cons: Pay-then-discover-seat-gone UX is terrible; SQL LIKE search and uncached event pages die on drop day.

Mid Ticketmaster: Redis event cache and TTL seat lock.
Mid architecture

Pros: Redis TTL hold (~10m) fixes checkout UX; event cache absorbs refresh storms; Stripe webhook stays the commit path. Cons: Hold TTL too long starves inventory; too short frustrates payers; confusing event-cache Redis with lock Redis causes ops mistakes.

Pro Ticketmaster: CDN, Elasticsearch CDC, and virtual waiting room.
Pro architecture

Pros: Waiting room protects booking under millions of concurrent checkouts; ES+CDC hits <500ms search; CDN helps the read shell. Cons: Waiting rooms are a product decision (fairness vs revenue); CDC lag can show sold-out events as available in search — booking remains truth.

Interview takeaway

Mid: working spine + no double book. Senior: hold + ES + cache. Staff: drop-day product/tech judgment.

Wrapping up

Ticketmaster is a CAP split: soft freshness for browse/search, hard exclusivity for seats — with timed holds, caches, a waiting room, and a search index making the 10M-user drop survivable.

Related: Delivery framework · Contention · Scaling reads · Caching · Elasticsearch · Real-time updates · PostgreSQL · Design Gopuff · Design Instagram.

← Lattice