Low Level Design: Movie Theater Booking System

A movie theater booking system must handle concurrent seat selection, pricing rules, and ticket lifecycle from browse to cancellation. This design covers the key schemas and mechanisms needed to make it reliable under load.

Show and Screen Schema

A show is a scheduled screening of a movie on a specific screen at a specific time. The show table captures the core scheduling data:

shows(show_id, movie_id, screen_id, start_time, end_time, status)
status: ENUM('scheduled','cancelled','completed')

Each screen has a fixed seat map. Seats are stored once per screen, not duplicated per show:

seats(seat_id, screen_id, row, column, type, base_price)
type: ENUM('standard','premium','recliner')

Seat Lock Mechanism

The hardest part of any booking system is preventing two users from buying the same seat at the same time. A database-only approach with SELECT FOR UPDATE works but creates hot-row contention. The better approach is Redis SETNX per seat reservation:

Key:   lock:show:{show_id}:seat:{seat_id}
Value: user_id
TTL:   600 seconds (10 minutes)

When a user selects a seat, the backend attempts SETNX. If it returns 0, the seat is already held by someone else and the UI shows it as unavailable. If the user abandons checkout, the TTL expires and the seat becomes available again automatically. On payment success, the lock is replaced by a confirmed ticket record and the Redis key is deleted.

Pricing Model

Ticket price is calculated at booking time and stored on the ticket record. Never recalculate at payment — price must be locked at the moment the user adds to cart. The formula:

final_price = base_price
            + type_surcharge(seat.type)
            + weekend_multiplier(show.start_time)
            + peak_hours_surcharge(show.start_time)

Typical surcharges: premium adds 20%, recliner adds 50%. Weekend multiplier applies Friday evening through Sunday. Peak hours (6pm-10pm) add a flat surcharge. These rules live in a config table so they can be updated without a deploy.

Ticket Schema and Payment Flow

The ticket table is the source of truth for a confirmed booking:

tickets(ticket_id, show_id, seat_id, user_id, price, status, qr_code, booked_at)
status: ENUM('held','confirmed','cancelled','used')

Payment flow steps:

  1. User selects seats → Redis SETNX locks acquired, ticket rows inserted with status held.
  2. User submits payment → payment service charges card.
  3. On payment success → ticket status updated to confirmed, QR code generated and stored.
  4. On payment failure → locks released, ticket rows deleted or marked cancelled.

QR Code Generation

The QR code encodes enough information for the scanner to verify authenticity without a network call. The payload is:

payload = ticket_id + ":" + show_id + ":" + seat_id
hmac    = HMAC-SHA256(payload, SECRET_KEY)
qr_data = base64(payload + ":" + hmac)

At the gate, the scanner decodes the QR, recomputes the HMAC, verifies it matches, then marks the ticket as used. This prevents forged QR codes while keeping gate validation fast even if the backend is briefly unreachable.

Cancellation Policy

Cancellation rules are time-based relative to show start:

  • More than 2 hours before show: full refund.
  • Within 2 hours of show: 50% refund.
  • After show start: no refund, ticket is non-cancellable.

Refund amount is calculated at cancellation time: refund = ticket.price * refund_rate(now, show.start_time). The refund is issued back to the original payment method via the payment service. Ticket status is set to cancelled and the seat becomes available for resale if more than 2 hours remain.

Concurrency Edge Cases

Two concurrent cancellations for the same seat should be idempotent — use a database-level status check with optimistic locking. Two users trying to grab the same expired lock should be handled by Redis — only one SETNX wins. Payment timeout: if the payment service does not respond within 30 seconds, treat as failure and release locks. All state transitions should be logged to an audit table for dispute resolution.

This design scales to a multiplex with dozens of screens and handles typical booking spikes (new release sales) by pushing contention onto Redis rather than the database.

{ “@context”: “https://schema.org”, “@type”: “FAQPage”, “mainEntity”: [ { “@type”: “Question”, “name”: “How do you implement seat locking to prevent double-booking in a movie theater system?”, “acceptedAnswer”: { “@type”: “Answer”, “text”: “Use Redis SETNX (SET if Not eXists) with a short TTL (e.g., 10 minutes) to place a distributed lock on each seat ID the moment a user begins checkout. The key is structured as seat_lock:{showtime_id}:{seat_id} and maps to the user session. If SETNX returns 0 the seat is already claimed; the system returns a conflict error immediately. A background job or TTL expiry releases unclaimed locks automatically, making the seat available again without manual cleanup.” } }, { “@type”: “Question”, “name”: “How are QR-code tickets generated securely for a theater booking platform?”, “acceptedAnswer”: { “@type”: “Answer”, “text”: “After payment confirmation, the server generates a signed payload containing booking_id, showtime_id, seat_ids, and an expiry timestamp. The payload is signed with HMAC-SHA256 using a server-side secret key. The resulting token is Base64URL-encoded and embedded into a QR code. At the gate, scanners decode the QR, recompute the HMAC with the same secret, and compare signatures. Because the secret never leaves the server, tickets cannot be forged or tampered with.” } }, { “@type”: “Question”, “name”: “How should payment holds and cancellations be handled in a theater booking system?”, “acceptedAnswer”: { “@type”: “Answer”, “text”: “When a user reaches the payment step, issue a payment authorization hold (not a capture) via the payment gateway. This reserves the funds without charging them. If checkout completes successfully, capture the hold. If the user abandons checkout or the seat lock expires, release the authorization hold via the gateway’s void API. For post-purchase cancellations within the allowed window, issue a refund against the captured charge. Store payment_intent_id and hold_status in the booking record to support idempotent retries on network failures.” } }, { “@type”: “Question”, “name”: “What concurrency control strategy prevents race conditions when two users try to book the same seat simultaneously?”, “acceptedAnswer”: { “@type”: “Answer”, “text”: “Apply optimistic concurrency at the database layer combined with the Redis distributed lock. The seats table has a version column. When finalizing a booking, the UPDATE statement includes WHERE seat_id = ? AND status = ‘available’ AND version = ?. If zero rows are affected, another transaction won the race and the current request is rejected. The Redis lock provides a fast first-pass filter so that the slower database round-trip is reached only by one request in most cases, reducing contention significantly.” } }, { “@type”: “Question”, “name”: “How should the showtime schema be designed to support a high-traffic theater booking system?”, “acceptedAnswer”: { “@type”: “Answer”, “text”: “Core entities: Movie (movie_id, title, duration_minutes, rating), Theater (theater_id, name, location), Screen (screen_id, theater_id, total_seats, layout_json), Showtime (showtime_id, movie_id, screen_id, start_time, end_time, base_price), Seat (seat_id, screen_id, row, number, type), and Booking (booking_id, showtime_id, user_id, seat_ids[], status, payment_intent_id, version). The layout_json on Screen stores the seat map as a 2-D grid for front-end rendering. Indexes on (showtime_id, status) on Seat allow fast availability queries. Partition Showtime by start_time range to keep historical data out of hot queries.” } } ] }

Frequently Asked Questions

Q: How do you implement seat locking to prevent double-booking in a movie theater system?

A: Use Redis SETNX (SET if Not eXists) with a short TTL (e.g., 10 minutes) to place a distributed lock on each seat ID the moment a user begins checkout. The key is structured as seat_lock:{showtime_id}:{seat_id} and maps to the user session. If SETNX returns 0 the seat is already claimed; the system returns a conflict error immediately. A background job or TTL expiry releases unclaimed locks automatically, making the seat available again without manual cleanup.

Q: How are QR-code tickets generated securely for a theater booking platform?

A: After payment confirmation, the server generates a signed payload containing booking_id, showtime_id, seat_ids, and an expiry timestamp. The payload is signed with HMAC-SHA256 using a server-side secret key. The resulting token is Base64URL-encoded and embedded into a QR code. At the gate, scanners decode the QR, recompute the HMAC with the same secret, and compare signatures. Because the secret never leaves the server, tickets cannot be forged or tampered with.

Q: How should payment holds and cancellations be handled in a theater booking system?

A: When a user reaches the payment step, issue a payment authorization hold (not a capture) via the payment gateway. This reserves the funds without charging them. If checkout completes successfully, capture the hold. If the user abandons checkout or the seat lock expires, release the authorization hold via the gateway’s void API. For post-purchase cancellations within the allowed window, issue a refund against the captured charge. Store payment_intent_id and hold_status in the booking record to support idempotent retries on network failures.

Q: What concurrency control strategy prevents race conditions when two users try to book the same seat simultaneously?

A: Apply optimistic concurrency at the database layer combined with the Redis distributed lock. The seats table has a version column. When finalizing a booking, the UPDATE statement includes WHERE seat_id = ? AND status = ‘available’ AND version = ?. If zero rows are affected, another transaction won the race and the current request is rejected. The Redis lock provides a fast first-pass filter so that the slower database round-trip is reached only by one request in most cases, reducing contention significantly.

Q: How should the showtime schema be designed to support a high-traffic theater booking system?

A: Core entities: Movie (movie_id, title, duration_minutes, rating), Theater (theater_id, name, location), Screen (screen_id, theater_id, total_seats, layout_json), Showtime (showtime_id, movie_id, screen_id, start_time, end_time, base_price), Seat (seat_id, screen_id, row, number, type), and Booking (booking_id, showtime_id, user_id, seat_ids[], status, payment_intent_id, version). The layout_json on Screen stores the seat map as a 2-D grid for front-end rendering. Indexes on (showtime_id, status) on Seat allow fast availability queries. Partition Showtime by start_time range to keep historical data out of hot queries.

See also: Meta Interview Guide 2026: Facebook, Instagram, WhatsApp Engineering

See also: Uber Interview Guide 2026: Dispatch Systems, Geospatial Algorithms, and Marketplace Engineering

See also: Atlassian Interview Guide

Scroll to Top