Low-Level Design: Cinema Ticket Booking System — Seat Selection, Concurrent Reservations, and Pricing

Core Entities

Cinema: cinema_id, name, address, timezone. Hall: hall_id, cinema_id, name, total_rows, total_cols, layout_type (STANDARD, IMAX, VIP). Seat: seat_id, hall_id, row_label (A-Z), col_number, seat_type (STANDARD, PREMIUM, RECLINER, WHEELCHAIR), is_active. Movie: movie_id, title, duration_minutes, rating (G/PG/PG-13/R), genre, language, release_date, poster_url. Showtime: showtime_id, movie_id, hall_id, start_time, end_time, status (SCHEDULED, OPEN, SOLD_OUT, CANCELLED), base_price, language, format (2D, 3D, IMAX, 4DX). SeatShowtime: seat_showtime_id, seat_id, showtime_id, status (AVAILABLE, RESERVED, BOOKED, BLOCKED), reservation_expires_at. Booking: booking_id, user_id, showtime_id, status (CONFIRMED, CANCELLED, EXPIRED), total_amount, payment_id, booked_at. BookingItem: item_id, booking_id, seat_id, seat_showtime_id, price.

Seat Reservation with Concurrency Control

class SeatReservationService:
    RESERVATION_TTL_MINUTES = 10

    def reserve_seats(self, user_id: int, showtime_id: int,
                      seat_ids: list[int]) -> Reservation:
        with self.db.transaction():
            # Lock rows for selected seats atomically
            seats = self.db.query(
                '''SELECT * FROM seat_showtimes
                   WHERE seat_id IN :ids AND showtime_id = :s
                   FOR UPDATE''',
                ids=seat_ids, s=showtime_id
            )
            # Check all are AVAILABLE
            unavailable = [s for s in seats if s.status != 'AVAILABLE']
            if unavailable:
                raise SeatsUnavailableError([s.seat_id for s in unavailable])

            expires_at = datetime.utcnow() + timedelta(
                minutes=self.RESERVATION_TTL_MINUTES
            )
            for seat in seats:
                self.db.execute(
                    '''UPDATE seat_showtimes SET status='RESERVED',
                       user_id=:u, reservation_expires_at=:e
                       WHERE seat_showtime_id=:id''',
                    u=user_id, e=expires_at, id=seat.seat_showtime_id
                )
            return Reservation(
                user_id=user_id, showtime_id=showtime_id,
                seat_ids=seat_ids, expires_at=expires_at
            )

    def release_expired_reservations(self):
        # Scheduled job runs every minute
        self.db.execute(
            '''UPDATE seat_showtimes SET status='AVAILABLE',
               user_id=NULL, reservation_expires_at=NULL
               WHERE status='RESERVED'
               AND reservation_expires_at < NOW()'''
        )

Dynamic Pricing

Seat price is not uniform — it varies by seat type, show format, time of day, and occupancy. Pricing factors: seat_type premium: STANDARD = base_price, PREMIUM = 1.3x, RECLINER = 1.6x, WHEELCHAIR = base_price. Format premium: IMAX = 1.5x, 3D = 1.2x, 4DX = 1.8x, 2D = 1.0x. Time-of-day: morning shows (before noon) = 0.85x, prime time (7-10pm) = 1.15x. Day-of-week: weekdays = 1.0x, weekends = 1.1x. Occupancy surge: > 80% booked = 1.1x, > 95% = 1.2x. Price = base_price * type_mult * format_mult * time_mult * day_mult * occupancy_mult. Price is calculated and displayed at seat selection time. It is locked in when the reservation is created and stored on BookingItem. Promotions: PromoCode table (code, discount_type (PERCENT/FIXED), discount_value, max_uses, uses_count, valid_until). Applied at checkout; decrement uses_count atomically (UPDATE WHERE uses_count < max_uses).

Seat Map Rendering and Availability

The seat selection UI requires showing which seats are available, reserved (temporarily held by others), and booked. Data: for a showtime, query all SeatShowtime rows joined with Seat (row, col, type, status). Response structure: a 2D grid of seat objects with their current status. Caching: seat map changes frequently (every reservation). Cache with a short TTL (5 seconds) or use server-sent events / WebSocket to push updates. Alternatively: compute the seat map on request but cache the hall layout (seat positions) separately, merging with live availability per request. Pagination: no pagination needed — a hall has at most ~500 seats, returning all in one response is fine. Hold visualization: seats in RESERVED status show as “held” (greyed out) with a countdown timer if the current user holds them. Aisle gaps: the Hall layout stores aisle columns (e.g., col 5 and col 11 are aisles). Seat map rendering skips these columns. Screen position: row A is the front row (near the screen). The UI typically displays row Z at the top (farthest from screen = most desirable for standard theaters).

Notifications and Post-Booking

On booking confirmation: send email with QR code ticket (PDF attachment). Send SMS with booking reference. Add to Google/Apple Wallet (Passbook). QR code encodes: booking_id + seat_ids + showtime_id + HMAC signature (prevents forgery). Validation at the venue: scanner decodes QR, verifies HMAC against venue’s secret key, checks booking status. Cancellation policy: full refund if cancelled > 3 hours before showtime. 50% refund if 1-3 hours before. No refund < 1 hour before. Cancellation updates booking status to CANCELLED, releases seat_showtimes back to AVAILABLE, processes refund via payment gateway. Waitlist: when a showtime is SOLD_OUT, users can join a waitlist. On cancellation, the next user on the waitlist is notified and given 15 minutes to complete booking.


{“@context”:”https://schema.org”,”@type”:”FAQPage”,”mainEntity”:[{“@type”:”Question”,”name”:”How do you prevent two users from booking the same seat simultaneously?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Use pessimistic locking (SELECT … FOR UPDATE) within a database transaction. This locks the SeatShowtime rows for the selected seats. Both concurrent transactions see the seats as AVAILABLE when they start, but the second transaction blocks until the first commits. After the first commits (setting status to RESERVED), the second transaction sees the updated status and returns an error.”}},{“@type”:”Question”,”name”:”What happens when a user abandons their seat reservation without paying?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Each reservation has a reservation_expires_at timestamp (e.g., 10 minutes from creation). A scheduled job runs every minute and executes: UPDATE seat_showtimes SET status='AVAILABLE' WHERE status='RESERVED' AND reservation_expires_at < NOW(). This releases expired holds back to the available pool automatically without requiring the user to explicitly cancel.”}},{“@type”:”Question”,”name”:”How do you calculate dynamic seat pricing?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Price = base_price * seat_type_multiplier * format_multiplier * time_of_day_multiplier * day_of_week_multiplier * occupancy_multiplier. The multipliers are configured per cinema or globally. The final price is computed at reservation time and stored on the BookingItem — it does not change after the reservation is created, even if occupancy surges before payment.”}},{“@type”:”Question”,”name”:”How are cinema QR code tickets secured against forgery?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”The QR code encodes: booking_id + seat_ids + showtime_id + an HMAC signature computed with a secret key shared between the ticketing backend and venue scanners. At the venue, the scanner decodes the QR, recomputes the HMAC with the same key, and verifies it matches. A forged ticket without the secret key will fail HMAC verification. The booking status is also checked in real time to catch cancelled or already-scanned tickets.”}},{“@type”:”Question”,”name”:”How does a waitlist work when a showtime is sold out?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”When all seats are BOOKED or RESERVED and a user tries to purchase, offer them a waitlist spot. When a cancellation occurs, the backend releases the seats and queries the waitlist in order (FIFO). The first waitlisted user is notified (push notification + email) and given a 15-minute window to complete their booking. If they do not book in time, the next user is notified.”}}]}

See also: Shopify Interview Prep

See also: Stripe Interview Prep

See also: Airbnb Interview Prep

Scroll to Top