Requirements


Functional Requirements:


  • Users search for movies/events by location, date, genre
  • Browse theaters, showtimes, and seat maps
  • Select seats (with real-time availability)
  • Hold seats temporarily during checkout
  • Process payments and issue e-tickets
  • Cancellation and refund flow
  • Admin panel for theater partners to manage listings, inventory, pricing



Non-Functional Requirements:


  • High concurrency: thousands of users competing for the same seats (e.g., a blockbuster release)
  • Low latency on seat availability checks
  • Strong consistency on booking — no double-selling a seat
  • High availability (targeting 99.99%)
  • Eventual consistency is acceptable for search/catalog but not for seat reservation



~50M monthly active users, ~5M daily

Peak: 50k+ concurrent seat selection requests for a hot show

~500k bookings/day, ~200M tickets/year

Average payload per booking: ~2KB → modest storage, but high write throughput during spikes


API Design

Search & Browse

  • GET /movies?city=&date=&genre= → movie listings
  • GET /movies/{id}/showtimes?date=&venue_id= → available showtimes
  • GET /showtimes/{id}/seats → seat map with real-time availability

Booking Flow

  • POST /showtimes/{id}/hold body: { seat_ids: [...] } → temporarily locks seats, returns a hold_token with TTL (say 7 min)
  • POST /bookings body: { hold_token, payment_info } → confirms booking, charges payment
  • DELETE /bookings/{id} → cancellation

Admin

  • POST /venues/{id}/showtimes → create showtime
  • PUT /showtimes/{id}/pricing → update pricing tiers




High-Level Design

User — id, name, email, phone, payment_methods

Movie/Event — id, title, genre, language, duration, rating, poster_url

Venue (Theater) — id, name, location (lat/lng), city, screens

Screen — id, venue_id, seat_map (rows × cols with categories like Gold/Silver)

Showtime — id, movie_id, screen_id, start_time, pricing_tiers

Seat Inventory — showtime_id + seat_id → status (AVAILABLE / HELD / BOOKED)

Booking — id, user_id, showtime_id, seats[], total_price, status, payment_id, created_at

Payment — id, booking_id, method, amount, status, gateway_txn_id



Detailed Component Design

Step 1: User views seat mapGET /showtimes/{id}/seats → Booking Service reads from Redis (seat status cache). Fast, but slightly stale is okay here — it's just a visual hint.

Step 2: User selects seats and hits "Book"POST /showtimes/{id}/hold { seat_ids: [A1, A2] } → Booking Service attempts an atomic operation:



sql

-- Postgres with row-level locking UPDATE seat_inventory SET status = 'HELD', hold_token = :token, hold_expires_at = NOW() + interval '7 min' WHERE showtime_id = :sid AND seat_id IN ('A1','A2') AND status = 'AVAILABLE'; -- Check affected rows == requested count, else rollback

This is the serialization point. Only one user wins. The loser gets a "seats unavailable" response immediately. Redis is updated after the write.

Step 3: User completes payment within hold windowPOST /bookings { hold_token, payment } → Payment Service charges the card → on success, Booking Service transitions seats from HELD → BOOKED. → Event published to Kafka → Notification Service sends confirmation email/SMS + Ticket Gen Service creates QR code PDF.

Step 4: Hold expires (user abandoned) → A background Hold Expiry Worker runs every 30s scanning for expired holds and flips them back to AVAILABLE. Alternatively, use Redis key TTL as a trigger.


Handling hot showtimes (the "Avengers problem"):

  • A single showtime's seats fit in a few KB, so a single Postgres row-level lock is viable up to a point.
  • For extreme spikes, shard seat inventory by showtime_id — each showtime's seats live on one shard, so the lock contention is isolated.
  • Add a virtual waiting room (queue-based admission) for mega-popular releases: users get a random position, admitted in batches. This converts a stampede into a controlled flow.

Redis usage:

  • Seat availability cache per showtime (refreshed on every write). Reads hit Redis, writes go to Postgres then invalidate Redis.
  • Hold token TTLs as a backup expiration mechanism.

Search:

  • Elasticsearch indexes movies, venues, showtimes. Updated asynchronously via CDC (Change Data Capture) from Postgres.
  • Geo queries for "theaters near me" using ES geo_point.

Payments:

  • Idempotency keys on every payment request to handle retries safely.
  • Two-phase: authorize during hold, capture on confirmation. If hold expires, release the auth.

Availability & fault tolerance:

  • Postgres primary-replica with synchronous replication for the booking path.
  • If Redis goes down, fall back to Postgres reads (slower but correct).
  • Payment failures → seats stay HELD until TTL expires, user can retry.
  • Circuit breaker on payment gateway calls.

Observability:

  • Track key metrics: seat hold success rate, payment conversion rate, hold expiry rate, p99 latency on the booking path.
  • Alert on double-book anomalies (should be zero, ever).