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.

See also: Shopify Interview Prep

See also: Stripe Interview Prep

See also: Airbnb Interview Prep

Scroll to Top