Design Movie Ticket Booking System

Building a production-grade movie ticket booking system like BookMyShow, Fandango, or AMC Theatres requires handling concurrent seat selection, inventory management, payment processing, and real-time availability updates at scale. This system must prevent double-booking, handle high traffic spikes during blockbuster releases, and provide a seamless user experience.

Step 1: Requirements Clarification

Functional Requirements

Core Booking Flow:

  • Browse movies currently showing and upcoming releases
  • Search showtimes by movie, location, date, and theater
  • View cinema hall layout with real-time seat availability
  • Select seats with temporary hold/lock mechanism
  • Complete booking with payment processing
  • Receive booking confirmation with QR code/ticket
  • Cancel bookings based on cancellation policy
  • View booking history and manage active bookings

Theater Management:

  • Manage multiple cinema chains and theater locations
  • Configure cinema halls with different seating layouts (regular, premium, recliners)
  • Set pricing tiers based on seat type, showtime, and day
  • Schedule showtimes with buffer time for cleaning
  • Block/unblock seats for maintenance

Additional Features:

  • Apply promotional codes and discounts
  • Food and beverage add-ons
  • Group booking with seat clustering
  • Waitlist for sold-out shows
  • Review and rating system
  • Push notifications for upcoming shows

Non-Functional Requirements

Performance:

  • Support 10,000+ concurrent users during peak times (Friday evening releases)
  • Seat selection response time < 200ms
  • Booking completion within 5 seconds
  • Handle 500+ bookings per second during blockbuster releases

Availability:

  • 99.95% uptime for booking system
  • Zero double-booking tolerance (strong consistency for seat inventory)
  • Graceful degradation when payment gateway is down

Scalability:

  • Support 50,000+ theaters globally
  • Handle 1M+ daily bookings
  • Store 5+ years of booking history
  • Scale horizontally for traffic spikes

Data Consistency:

  • Strong consistency for seat inventory (no double-booking)
  • Eventual consistency acceptable for movie metadata, reviews
  • ACID guarantees for payment transactions
  • Idempotent booking operations

Scale Estimations

Traffic:

  • 100M registered users
  • 10M daily active users
  • 1M bookings per day (peak: 100 bookings/second)
  • Average 2-3 seats per booking
  • Read:Write ratio of 100:1 (lots of browsing, fewer bookings)

Storage:

  • Movies: 10,000 active movies × 10KB = 100MB
  • Theaters: 50,000 theaters × 5KB = 250MB
  • Cinema Halls: 200,000 halls × 20KB = 4GB (with seat layouts)
  • Showtimes: 1M active showtimes × 5KB = 5GB
  • Bookings: 1M/day × 5KB × 365 × 5 years = 9TB
  • Seat inventory: 1M showtimes × 200 seats × 100 bytes = 20GB
  • Total: ~10TB over 5 years (excluding media assets)

Step 2: High-Level Design

System Architecture

┌─────────────────────────────────────────────────────────────────┐
│                         Client Layer                             │
│  (Web App, Mobile Apps, Kiosks, Partner Integrations)           │
└───────────────────────────────┬─────────────────────────────────┘

┌───────────────────────────────▼─────────────────────────────────┐
│                       API Gateway + CDN                          │
│  (Rate Limiting, Auth, Routing, Static Assets)                  │
└───────────────────────────────┬─────────────────────────────────┘

        ┌───────────────────────┼───────────────────────┐
        │                       │                       │
┌───────▼────────┐   ┌─────────▼────────┐   ┌─────────▼─────────┐
│  Movie Service │   │ Theater Service  │   │ Showtime Service  │
│                │   │                  │   │                   │
│ - Movie list   │   │ - Theater info   │   │ - Schedule mgmt   │
│ - Search       │   │ - Hall layouts   │   │ - Availability    │
│ - Metadata     │   │ - Seat config    │   │ - Capacity calc   │
└────────────────┘   └──────────────────┘   └───────────────────┘

                    ┌───────────┴───────────┐
                    │                       │
        ┌───────────▼────────┐   ┌─────────▼──────────┐
        │  Booking Service   │   │   Seat Service     │
        │                    │   │                    │
        │ - Booking flow     │   │ - Seat locking     │
        │ - State machine    │   │ - Availability     │
        │ - Timeout mgmt     │   │ - Lock TTL         │
        └──────────┬─────────┘   └─────────┬──────────┘
                   │                       │
        ┌──────────┴─────────┬─────────────┘
        │                    │
┌───────▼────────┐   ┌───────▼────────┐   ┌──────────────────┐
│ Payment Service│   │ Notification   │   │ Analytics Service│
│                │   │    Service     │   │                  │
│ - Gateway integ│   │ - Email/SMS    │   │ - Booking stats  │
│ - Refunds      │   │ - Push notif   │   │ - Revenue track  │
│ - Reconciliation│  │ - QR code gen  │   │ - Occupancy      │
└────────────────┘   └────────────────┘   └──────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│                         Data Layer                               │
│                                                                  │
│  ┌────────────────┐  ┌─────────────┐  ┌────────────────────┐   │
│  │   PostgreSQL   │  │    Redis    │  │   ElasticSearch    │   │
│  │                │  │             │  │                    │   │
│  │ - Bookings     │  │ - Seat locks│  │ - Movie search     │   │
│  │ - Seats        │  │ - Session   │  │ - Theater search   │   │
│  │ - Showtimes    │  │ - Cache     │  │ - Showtime filter  │   │
│  │ - Payments     │  │ - Dist lock │  │                    │   │
│  └────────────────┘  └─────────────┘  └────────────────────┘   │
│                                                                  │
│  ┌────────────────┐  ┌─────────────┐                            │
│  │     Kafka      │  │   S3/CDN    │                            │
│  │                │  │             │                            │
│  │ - Booking evts │  │ - Posters   │                            │
│  │ - Payment evts │  │ - Trailers  │                            │
│  │ - Analytics    │  │ - QR codes  │                            │
│  └────────────────┘  └─────────────┘                            │
└─────────────────────────────────────────────────────────────────┘

Core Services

1. Movie Service

  • Manages movie catalog (metadata, cast, crew, ratings)
  • Provides search and filtering capabilities
  • Integrates with content providers for trailers, posters
  • Caches heavily (movie data changes infrequently)

2. Theater Service

  • Manages theater locations and cinema halls
  • Stores hall layouts with seat configurations
  • Handles seat types (regular, premium, recliner, wheelchair)
  • Manages pricing tiers and dynamic pricing rules

3. Showtime Service

  • Schedules movie showtimes across theaters
  • Manages showtime capacity and availability
  • Handles buffer times between shows
  • Coordinates with booking service for capacity updates

4. Seat Service

  • Manages real-time seat availability
  • Implements distributed seat locking with TTL
  • Prevents concurrent booking conflicts
  • Handles seat blocking for maintenance

5. Booking Service

  • Orchestrates end-to-end booking workflow
  • Implements booking state machine
  • Manages booking timeout (typically 10 minutes)
  • Handles cancellations and refunds
  • Ensures idempotency for retry scenarios

6. Payment Service

  • Integrates with payment gateways (Stripe, PayPal, etc.)
  • Handles payment processing and confirmation
  • Manages refund workflows
  • Implements payment reconciliation
  • Stores payment audit trail

API Design

Key Endpoints:

// Movie & Showtime Discovery
GET  /api/v1/movies?location={city}&date={date}
GET  /api/v1/movies/{movieId}/showtimes?theater={id}&date={date}
GET  /api/v1/theaters?location={city}&movie={id}

// Seat Selection
GET  /api/v1/showtimes/{showtimeId}/seats
POST /api/v1/showtimes/{showtimeId}/seats/lock
     Body: { seatIds: ["A1", "A2"], sessionId: "uuid" }
DELETE /api/v1/showtimes/{showtimeId}/seats/lock/{sessionId}

// Booking Flow
POST /api/v1/bookings/initiate
     Body: { showtimeId, seatIds, userId, sessionId }
POST /api/v1/bookings/{bookingId}/confirm
     Body: { paymentId, paymentMethod }
GET  /api/v1/bookings/{bookingId}
DELETE /api/v1/bookings/{bookingId}/cancel

// Payment
POST /api/v1/payments/process
     Body: { bookingId, amount, method, token }
POST /api/v1/payments/{paymentId}/refund

Step 3: Deep Dives

3.1 Seat Inventory Management with Locking

The most critical component is preventing double-booking while providing a smooth user experience. We need a two-tier locking mechanism.

Database Schema (PostgreSQL):

-- Theater and Hall Configuration
CREATE TABLE theaters (
    id BIGSERIAL PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    location_city VARCHAR(100),
    location_address TEXT,
    chain_id BIGINT,
    created_at TIMESTAMP DEFAULT NOW()
);

CREATE TABLE cinema_halls (
    id BIGSERIAL PRIMARY KEY,
    theater_id BIGINT REFERENCES theaters(id),
    name VARCHAR(100),
    total_seats INT,
    layout_config JSONB,  -- Store seat map structure
    created_at TIMESTAMP DEFAULT NOW()
);

-- Seat Configuration (Template)
CREATE TABLE seats (
    id BIGSERIAL PRIMARY KEY,
    hall_id BIGINT REFERENCES cinema_halls(id),
    seat_number VARCHAR(10),  -- e.g., "A1", "B5"
    row_number VARCHAR(5),
    seat_type VARCHAR(50),    -- regular, premium, recliner, wheelchair
    is_active BOOLEAN DEFAULT true,
    UNIQUE(hall_id, seat_number)
);

-- Showtime with Capacity Tracking
CREATE TABLE showtimes (
    id BIGSERIAL PRIMARY KEY,
    movie_id BIGINT,
    hall_id BIGINT REFERENCES cinema_halls(id),
    start_time TIMESTAMP,
    end_time TIMESTAMP,
    base_price DECIMAL(10,2),
    total_seats INT,
    available_seats INT,
    status VARCHAR(50),  -- scheduled, open, started, ended, cancelled
    created_at TIMESTAMP DEFAULT NOW(),
    INDEX idx_movie_time (movie_id, start_time),
    INDEX idx_hall_time (hall_id, start_time)
);

-- Seat Inventory (Per Showtime)
CREATE TABLE showtime_seats (
    id BIGSERIAL PRIMARY KEY,
    showtime_id BIGINT REFERENCES showtimes(id),
    seat_id BIGINT REFERENCES seats(id),
    status VARCHAR(50),   -- available, locked, booked, blocked
    locked_by VARCHAR(100), -- sessionId
    locked_at TIMESTAMP,
    booking_id BIGINT,
    price DECIMAL(10,2),
    version INT DEFAULT 0,  -- Optimistic locking
    UNIQUE(showtime_id, seat_id),
    INDEX idx_showtime_status (showtime_id, status)
);

-- Bookings with State Machine
CREATE TABLE bookings (
    id BIGSERIAL PRIMARY KEY,
    booking_reference VARCHAR(20) UNIQUE,
    user_id BIGINT,
    showtime_id BIGINT REFERENCES showtimes(id),
    session_id VARCHAR(100),
    status VARCHAR(50),  -- initiated, payment_pending, confirmed, cancelled, expired
    total_amount DECIMAL(10,2),
    seats_count INT,
    payment_id BIGINT,
    expires_at TIMESTAMP,
    confirmed_at TIMESTAMP,
    cancelled_at TIMESTAMP,
    created_at TIMESTAMP DEFAULT NOW(),
    updated_at TIMESTAMP DEFAULT NOW(),
    INDEX idx_user_status (user_id, status),
    INDEX idx_expires (expires_at, status)
);

CREATE TABLE booking_seats (
    booking_id BIGINT REFERENCES bookings(id),
    seat_id BIGINT,
    seat_number VARCHAR(10),
    price DECIMAL(10,2),
    PRIMARY KEY (booking_id, seat_id)
);

Two-Tier Locking Strategy:

Tier 1: Redis Distributed Locks (User Selection Phase)

When a user selects seats, acquire temporary locks in Redis:

class SeatLockingService:
    def __init__(self, redis_client, postgres_db):
        self.redis = redis_client
        self.db = postgres_db
        self.LOCK_TTL = 600  # 10 minutes
        self.LOCK_EXTEND_THRESHOLD = 120  # 2 minutes

    def lock_seats(self, showtime_id: int, seat_ids: list, session_id: str):
        """
        Acquire distributed locks on seats using Redis
        Returns: (success: bool, locked_seats: list, error: str)
        """
        lock_keys = [f"seat_lock:{showtime_id}:{seat_id}" for seat_id in seat_ids]

        # Use Redis pipeline for atomic multi-seat locking
        pipe = self.redis.pipeline()

        # First, check if any seats are already locked or booked
        for seat_id in seat_ids:
            seat_key = f"seat_lock:{showtime_id}:{seat_id}"
            existing_lock = self.redis.get(seat_key)

            if existing_lock and existing_lock != session_id:
                return False, [], "One or more seats already locked"

        # Check database for confirmed bookings
        booked_seats = self.db.execute("""
            SELECT seat_id FROM showtime_seats
            WHERE showtime_id = %s AND seat_id = ANY(%s)
            AND status = 'booked'
        """, (showtime_id, seat_ids))

        if booked_seats:
            return False, [], "One or more seats already booked"

        # Acquire locks atomically
        for seat_id in seat_ids:
            seat_key = f"seat_lock:{showtime_id}:{seat_id}"
            pipe.set(seat_key, session_id, ex=self.LOCK_TTL, nx=True)

        results = pipe.execute()

        # Check if all locks acquired successfully
        if not all(results):
            # Rollback: release any acquired locks
            self.unlock_seats(showtime_id, seat_ids, session_id)
            return False, [], "Failed to acquire all seat locks"

        # Update seat status in database optimistically
        try:
            self.db.execute("""
                UPDATE showtime_seats
                SET status = 'locked',
                    locked_by = %s,
                    locked_at = NOW(),
                    version = version + 1
                WHERE showtime_id = %s
                AND seat_id = ANY(%s)
                AND status = 'available'
            """, (session_id, showtime_id, seat_ids))

            self.db.commit()
        except Exception as e:
            # Rollback locks on database error
            self.unlock_seats(showtime_id, seat_ids, session_id)
            return False, [], f"Database error: {str(e)}"

        return True, seat_ids, None

    def unlock_seats(self, showtime_id: int, seat_ids: list, session_id: str):
        """Release locks held by this session"""
        pipe = self.redis.pipeline()

        for seat_id in seat_ids:
            seat_key = f"seat_lock:{showtime_id}:{seat_id}"
            # Only delete if lock belongs to this session (Lua script for atomicity)
            lua_script = """
                if redis.call("get", KEYS[1]) == ARGV[1] then
                    return redis.call("del", KEYS[1])
                else
                    return 0
                end
            """
            pipe.eval(lua_script, 1, seat_key, session_id)

        pipe.execute()

        # Update database
        self.db.execute("""
            UPDATE showtime_seats
            SET status = 'available',
                locked_by = NULL,
                locked_at = NULL
            WHERE showtime_id = %s
            AND seat_id = ANY(%s)
            AND locked_by = %s
            AND status = 'locked'
        """, (showtime_id, seat_ids, session_id))
        self.db.commit()

    def extend_lock(self, showtime_id: int, seat_ids: list, session_id: str):
        """Extend lock TTL (called periodically from client)"""
        for seat_id in seat_ids:
            seat_key = f"seat_lock:{showtime_id}:{seat_id}"
            current_lock = self.redis.get(seat_key)

            if current_lock == session_id:
                self.redis.expire(seat_key, self.LOCK_TTL)

Tier 2: PostgreSQL Row Locks (Payment Phase)

When payment is initiated, convert Redis locks to database-level locks:

def initiate_booking(self, user_id: int, showtime_id: int,
                     seat_ids: list, session_id: str):
    """
    Convert seat locks to booking with pessimistic locking
    """
    with self.db.transaction() as txn:
        # Pessimistic locking: lock rows for update
        seats = txn.execute("""
            SELECT id, seat_id, status, locked_by, price
            FROM showtime_seats
            WHERE showtime_id = %s
            AND seat_id = ANY(%s)
            FOR UPDATE NOWAIT  -- Fail fast if locked
        """, (showtime_id, seat_ids))

        # Verify all seats are locked by this session
        for seat in seats:
            if seat['status'] != 'locked' or seat['locked_by'] != session_id:
                raise BookingException("Seat no longer available")

        # Create booking record
        booking_id = txn.execute("""
            INSERT INTO bookings (
                booking_reference, user_id, showtime_id, session_id,
                status, total_amount, seats_count, expires_at
            ) VALUES (
                %s, %s, %s, %s, 'payment_pending', %s, %s,
                NOW() + INTERVAL '10 minutes'
            ) RETURNING id
        """, (
            generate_booking_reference(),
            user_id, showtime_id, session_id,
            sum(s['price'] for s in seats),
            len(seat_ids)
        ))

        # Link seats to booking
        for seat in seats:
            txn.execute("""
                INSERT INTO booking_seats (booking_id, seat_id, seat_number, price)
                VALUES (%s, %s, %s, %s)
            """, (booking_id, seat['seat_id'], seat['seat_number'], seat['price']))

        txn.commit()
        return booking_id

3.2 Showtime Scheduling with Capacity Management

Capacity Tracking:

class ShowtimeService:
    def create_showtime(self, movie_id: int, hall_id: int,
                       start_time: datetime, base_price: Decimal):
        """Create showtime and initialize seat inventory"""

        # Get hall configuration
        hall = self.db.get_hall(hall_id)
        seats = self.db.get_seats(hall_id)

        with self.db.transaction() as txn:
            # Create showtime
            showtime_id = txn.execute("""
                INSERT INTO showtimes (
                    movie_id, hall_id, start_time, end_time,
                    base_price, total_seats, available_seats, status
                ) VALUES (%s, %s, %s, %s, %s, %s, %s, 'scheduled')
                RETURNING id
            """, (
                movie_id, hall_id, start_time,
                start_time + timedelta(hours=3),  # Assume 3hr duration
                base_price, len(seats), len(seats)
            ))

            # Initialize seat inventory for this showtime
            for seat in seats:
                price = self.calculate_seat_price(seat, base_price, start_time)
                txn.execute("""
                    INSERT INTO showtime_seats (
                        showtime_id, seat_id, status, price
                    ) VALUES (%s, %s, 'available', %s)
                """, (showtime_id, seat['id'], price))

            txn.commit()
            return showtime_id

    def calculate_seat_price(self, seat: dict, base_price: Decimal,
                            showtime: datetime) -> Decimal:
        """Dynamic pricing based on seat type and time"""
        multiplier = 1.0

        # Seat type pricing
        if seat['seat_type'] == 'premium':
            multiplier *= 1.5
        elif seat['seat_type'] == 'recliner':
            multiplier *= 2.0

        # Weekend pricing
        if showtime.weekday() >= 5:  # Saturday, Sunday
            multiplier *= 1.3

        # Prime time pricing (6 PM - 11 PM)
        if 18 <= showtime.hour <= 23:
            multiplier *= 1.2

        return base_price * Decimal(str(multiplier))

3.3 Seat Selection with Real-Time Availability

Cinema Hall Layout Representation:

# Store layout as JSONB in database
layout_config = {
    "rows": 15,
    "columns": 20,
    "aisles": [5, 15],  # Column indices for aisles
    "sections": {
        "regular": {"rows": "A-J", "price_multiplier": 1.0},
        "premium": {"rows": "K-M", "price_multiplier": 1.5},
        "recliner": {"rows": "N-O", "price_multiplier": 2.0}
    },
    "blocked_seats": ["A1", "A20", "O10"],  # Wheelchair, broken, etc.
    "screen_position": "front"
}

class SeatMapService:
    def get_seat_availability(self, showtime_id: int) -> dict:
        """Get real-time seat map with availability"""

        # Check Redis cache first
        cache_key = f"seat_map:{showtime_id}"
        cached = self.redis.get(cache_key)
        if cached:
            return json.loads(cached)

        # Query database
        seats = self.db.execute("""
            SELECT
                s.seat_number,
                s.row_number,
                s.seat_type,
                ss.status,
                ss.price,
                ss.locked_by
            FROM showtime_seats ss
            JOIN seats s ON ss.seat_id = s.id
            WHERE ss.showtime_id = %s
            ORDER BY s.row_number, s.seat_number
        """, (showtime_id,))

        # Build seat map
        seat_map = self._build_seat_map(seats)

        # Cache for 30 seconds (balance freshness vs load)
        self.redis.setex(cache_key, 30, json.dumps(seat_map))

        return seat_map

    def _build_seat_map(self, seats: list) -> dict:
        """Transform flat seat list into 2D grid"""
        rows = {}
        for seat in seats:
            row = seat['row_number']
            if row not in rows:
                rows[row] = []

            rows[row].append({
                'number': seat['seat_number'],
                'type': seat['seat_type'],
                'status': self._get_display_status(seat),
                'price': float(seat['price'])
            })

        return {
            'rows': rows,
            'legend': {
                'available': 'Available for booking',
                'selected': 'Selected by you',
                'locked': 'Held by another user',
                'booked': 'Already booked',
                'blocked': 'Not available'
            }
        }

    def _get_display_status(self, seat: dict) -> str:
        """Map internal status to user-facing status"""
        if seat['status'] == 'booked':
            return 'booked'
        elif seat['status'] == 'blocked':
            return 'blocked'
        elif seat['status'] == 'locked':
            # Don't show which user locked it (privacy)
            return 'locked'
        else:
            return 'available'

3.4 Booking Workflow with Timeout

Booking State Machine:

┌──────────┐   lock_seats    ┌──────────────┐   initiate   ┌─────────────────┐
│   IDLE   │ ──────────────> │ SEATS_LOCKED │ ──────────> │ PAYMENT_PENDING │
└──────────┘                 └──────────────┘              └────────┬────────┘
                                     │                              │
                                     │ timeout                      │ confirm
                                     │ (10 min)                     │
                                     ▼                              ▼
                              ┌──────────┐                   ┌───────────┐
                              │ EXPIRED  │                   │ CONFIRMED │
                              └──────────┘                   └───────────┘

                                                                    │ cancel

                                                             ┌───────────┐
                                                             │ CANCELLED │
                                                             └───────────┘

State Machine Implementation:

class BookingStateMachine:
    TRANSITIONS = {
        'initiated': ['payment_pending', 'expired'],
        'payment_pending': ['confirmed', 'expired', 'cancelled'],
        'confirmed': ['cancelled'],
        'expired': [],
        'cancelled': []
    }

    def transition(self, booking_id: int, from_status: str,
                   to_status: str, **kwargs):
        """Execute state transition with validation"""

        if to_status not in self.TRANSITIONS.get(from_status, []):
            raise InvalidTransitionException(
                f"Cannot transition from {from_status} to {to_status}"
            )

        with self.db.transaction() as txn:
            # Pessimistic lock on booking
            booking = txn.execute("""
                SELECT * FROM bookings
                WHERE id = %s
                FOR UPDATE
            """, (booking_id,))[0]

            # Verify current status
            if booking['status'] != from_status:
                raise ConcurrentModificationException(
                    f"Booking status changed: expected {from_status}, "
                    f"got {booking['status']}"
                )

            # Execute transition handler
            handler = getattr(self, f"_handle_{to_status}", None)
            if handler:
                handler(txn, booking, **kwargs)

            # Update status
            txn.execute("""
                UPDATE bookings
                SET status = %s, updated_at = NOW()
                WHERE id = %s
            """, (to_status, booking_id))

            txn.commit()

    def _handle_confirmed(self, txn, booking, payment_id: int):
        """Confirm booking after successful payment"""
        # Update booking
        txn.execute("""
            UPDATE bookings
            SET payment_id = %s,
                confirmed_at = NOW()
            WHERE id = %s
        """, (payment_id, booking['id']))

        # Mark seats as booked
        txn.execute("""
            UPDATE showtime_seats
            SET status = 'booked',
                booking_id = %s,
                locked_by = NULL,
                locked_at = NULL
            WHERE showtime_id = %s
            AND seat_id IN (
                SELECT seat_id FROM booking_seats WHERE booking_id = %s
            )
        """, (booking['id'], booking['showtime_id'], booking['id']))

        # Update showtime capacity
        seats_count = booking['seats_count']
        txn.execute("""
            UPDATE showtimes
            SET available_seats = available_seats - %s
            WHERE id = %s
        """, (seats_count, booking['showtime_id']))

        # Release Redis locks
        self._release_locks(booking)

        # Publish event for notifications
        self.event_bus.publish('booking.confirmed', {
            'booking_id': booking['id'],
            'user_id': booking['user_id'],
            'showtime_id': booking['showtime_id']
        })

    def _handle_expired(self, txn, booking):
        """Handle booking expiration (timeout)"""
        # Release seats
        txn.execute("""
            UPDATE showtime_seats
            SET status = 'available',
                locked_by = NULL,
                locked_at = NULL
            WHERE showtime_id = %s
            AND seat_id IN (
                SELECT seat_id FROM booking_seats WHERE booking_id = %s
            )
        """, (booking['showtime_id'], booking['id']))

        # Release Redis locks
        self._release_locks(booking)

Timeout Management (Background Job):

class BookingTimeoutHandler:
    def __init__(self):
        self.check_interval = 60  # Check every minute

    def run(self):
        """Background job to expire timed-out bookings"""
        while True:
            try:
                # Find expired bookings
                expired = self.db.execute("""
                    SELECT id, status FROM bookings
                    WHERE status IN ('payment_pending')
                    AND expires_at < NOW()
                    LIMIT 1000
                """)

                for booking in expired:
                    try:
                        self.state_machine.transition(
                            booking['id'],
                            booking['status'],
                            'expired'
                        )
                    except Exception as e:
                        self.logger.error(
                            f"Failed to expire booking {booking['id']}: {e}"
                        )

                time.sleep(self.check_interval)

            except Exception as e:
                self.logger.error(f"Timeout handler error: {e}")
                time.sleep(self.check_interval)

3.5 Payment Processing and Confirmation

Payment Integration:

class PaymentService:
    def process_payment(self, booking_id: int, payment_method: str,
                       payment_token: str) -> dict:
        """Process payment through gateway"""

        # Get booking details
        booking = self.db.get_booking(booking_id)

        if booking['status'] != 'payment_pending':
            raise PaymentException("Booking not in payment_pending state")

        # Idempotency check
        idempotency_key = f"payment:{booking_id}"
        existing = self.redis.get(idempotency_key)
        if existing:
            return json.loads(existing)

        try:
            # Call payment gateway (e.g., Stripe)
            payment_response = self.gateway.charge(
                amount=booking['total_amount'],
                currency='USD',
                source=payment_token,
                metadata={
                    'booking_id': booking_id,
                    'user_id': booking['user_id']
                },
                idempotency_key=idempotency_key
            )

            # Store payment record
            payment_id = self.db.execute("""
                INSERT INTO payments (
                    booking_id, gateway_transaction_id, amount,
                    payment_method, status, response_data
                ) VALUES (%s, %s, %s, %s, 'success', %s)
                RETURNING id
            """, (
                booking_id,
                payment_response['id'],
                booking['total_amount'],
                payment_method,
                json.dumps(payment_response)
            ))

            # Confirm booking
            self.booking_service.confirm_booking(booking_id, payment_id)

            # Cache result for idempotency
            result = {'payment_id': payment_id, 'status': 'success'}
            self.redis.setex(idempotency_key, 3600, json.dumps(result))

            return result

        except PaymentGatewayException as e:
            # Store failed payment attempt
            self.db.execute("""
                INSERT INTO payments (
                    booking_id, amount, payment_method, status, error_message
                ) VALUES (%s, %s, %s, 'failed', %s)
            """, (booking_id, booking['total_amount'], payment_method, str(e)))

            raise PaymentException(f"Payment failed: {str(e)}")

3.6 Cancellation Policy and Refunds

Cancellation Rules:

class CancellationService:
    def cancel_booking(self, booking_id: int, user_id: int,
                      reason: str = None) -> dict:
        """Cancel confirmed booking with refund calculation"""

        booking = self.db.get_booking(booking_id)

        # Verify ownership
        if booking['user_id'] != user_id:
            raise UnauthorizedException()

        # Check if cancellable
        if booking['status'] not in ['confirmed', 'payment_pending']:
            raise CancellationException("Booking cannot be cancelled")

        showtime = self.db.get_showtime(booking['showtime_id'])
        time_until_show = showtime['start_time'] - datetime.now()

        # Determine refund amount based on policy
        refund_amount, refund_percentage = self._calculate_refund(
            booking['total_amount'],
            time_until_show
        )

        with self.db.transaction() as txn:
            # Update booking status
            self.state_machine.transition(
                booking_id,
                booking['status'],
                'cancelled',
                refund_amount=refund_amount
            )

            # Release seats
            txn.execute("""
                UPDATE showtime_seats
                SET status = 'available',
                    booking_id = NULL
                WHERE showtime_id = %s
                AND booking_id = %s
            """, (booking['showtime_id'], booking_id))

            # Update capacity
            txn.execute("""
                UPDATE showtimes
                SET available_seats = available_seats + %s
                WHERE id = %s
            """, (booking['seats_count'], booking['showtime_id']))

            # Process refund if applicable
            if refund_amount > 0 and booking['payment_id']:
                refund_id = self.payment_service.process_refund(
                    booking['payment_id'],
                    refund_amount
                )

            txn.commit()

        return {
            'cancelled': True,
            'refund_amount': refund_amount,
            'refund_percentage': refund_percentage
        }

    def _calculate_refund(self, total_amount: Decimal,
                         time_until_show: timedelta) -> tuple:
        """Calculate refund based on cancellation policy"""
        hours_until = time_until_show.total_seconds() / 3600

        if hours_until >= 24:
            # Full refund minus processing fee
            refund_pct = 0.95
        elif hours_until >= 4:
            # 50% refund
            refund_pct = 0.50
        elif hours_until >= 1:
            # 25% refund
            refund_pct = 0.25
        else:
            # No refund
            refund_pct = 0.0

        refund_amount = total_amount * Decimal(str(refund_pct))
        return refund_amount, refund_pct * 100

3.7 Concurrent Booking Prevention

Optimistic Locking for Edge Cases:

class ConcurrencyHandler:
    def handle_concurrent_booking_attempt(self, showtime_id: int,
                                         seat_ids: list, session_id: str):
        """
        Handle race conditions with optimistic locking + version checking
        """
        max_retries = 3
        retry_count = 0

        while retry_count < max_retries:
            try:
                with self.db.transaction(isolation='SERIALIZABLE') as txn:
                    # Lock seats with version check
                    seats = txn.execute("""
                        SELECT id, seat_id, status, version
                        FROM showtime_seats
                        WHERE showtime_id = %s
                        AND seat_id = ANY(%s)
                        FOR UPDATE
                    """, (showtime_id, seat_ids))

                    # Verify all available
                    for seat in seats:
                        if seat['status'] != 'available':
                            raise SeatUnavailableException(
                                f"Seat {seat['seat_id']} not available"
                            )

                    # Update with version increment (optimistic lock)
                    updated = txn.execute("""
                        UPDATE showtime_seats
                        SET status = 'locked',
                            locked_by = %s,
                            locked_at = NOW(),
                            version = version + 1
                        WHERE showtime_id = %s
                        AND seat_id = ANY(%s)
                        AND status = 'available'
                        RETURNING id
                    """, (session_id, showtime_id, seat_ids))

                    if len(updated) != len(seat_ids):
                        raise ConcurrentModificationException(
                            "Seats were modified concurrently"
                        )

                    txn.commit()
                    return True

            except SerializationException:
                retry_count += 1
                if retry_count >= max_retries:
                    raise ConcurrencyException(
                        "Too many concurrent attempts, please try again"
                    )
                time.sleep(0.1 * retry_count)  # Exponential backoff

        return False

3.8 Observability and Monitoring

Key Metrics to Track:

# Business Metrics
- Booking conversion rate (locks → confirmed bookings)
- Average booking time (lock → payment)
- Seat utilization rate per showtime
- Revenue per showtime / theater / movie
- Cancellation rate and reasons

# Technical Metrics
- Seat lock acquisition latency (p50, p95, p99)
- Booking API latency
- Lock timeout rate
- Payment success rate
- Database connection pool usage
- Redis cache hit rate
- Concurrent booking conflicts (retries)

# Alerts
- Double booking detected (CRITICAL)
- Payment gateway downtime
- Lock timeout rate > 5%
- Database replication lag > 10s
- Available seats mismatch (cache vs DB)

Step 4: Wrap-Up

Key Design Decisions

1. Two-Tier Locking Strategy

  • Redis for temporary seat selection (fast, with TTL)
  • PostgreSQL row locks for payment phase (ACID guarantees)
  • Prevents double-booking while maintaining performance

2. Strong Consistency for Inventory

  • Use PostgreSQL with SERIALIZABLE isolation for critical operations
  • Optimistic locking with version numbers for concurrent updates
  • Accept higher latency for correctness

3. Eventual Consistency for Reads

  • Cache seat maps in Redis (30s TTL)
  • Accept stale data for browsing (movie listings, theater info)
  • Refresh on lock attempt for accuracy

4. Booking State Machine

  • Clear state transitions with validation
  • Timeout handling via background jobs
  • Idempotent operations for retry safety

5. Horizontal Scalability

  • Stateless services (scale API servers independently)
  • Database sharding by theater_id or geographic region
  • Read replicas for movie/theater catalog queries
  • Redis cluster for distributed locking

Trade-offs

Performance vs Consistency:

  • Chose strong consistency for seat inventory over performance
  • Acceptable because booking flow is inherently synchronous
  • Users expect real-time accuracy for seat availability

Lock Duration:

  • 10-minute timeout balances user experience vs inventory availability
  • Too short: users can’t complete payment
  • Too long: seats held unnecessarily, reducing availability

Caching Strategy:

  • Aggressive caching for movie metadata (hours TTL)
  • Short cache for seat maps (30s) to balance load and freshness
  • No caching for booking state (always fresh)

Bottlenecks and Mitigations

1. Database Hot Spots

  • Problem: Popular showtimes cause high contention on seat rows
  • Solution: Connection pooling, read replicas, row-level locking instead of table locks

2. Lock Storms

  • Problem: Many users trying to book same seats simultaneously
  • Solution: Fail fast with NOWAIT, exponential backoff, queue system for waitlist

3. Payment Gateway Latency

  • Problem: External API calls slow down booking flow
  • Solution: Async payment processing, webhook for confirmation, optimistic UI updates

Future Enhancements

1. Recommendation Engine

  • Suggest seats based on user preferences (aisle, center, back row)
  • ML model trained on booking patterns

2. Dynamic Pricing

  • Real-time price adjustments based on demand
  • Surge pricing for blockbuster releases
  • Early bird discounts

3. Waitlist Management

  • Queue system for sold-out shows
  • Automatic notification when seats become available (cancellations)

4. Multi-Region Support

  • Geographic distribution for global scale
  • Region-specific pricing, languages, payment methods
  • Data residency compliance (GDPR, etc.)

5. Advanced Analytics

  • Predictive occupancy modeling
  • Optimal showtime scheduling
  • Theater layout optimization based on booking patterns

This design provides a robust, scalable foundation for a production movie ticket booking system, handling millions of concurrent users while preventing double-booking and ensuring a seamless user experience.