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.
Comments