
Durgesh Tiwari
Author
Designing a Ticket Booking System like BookMyShow is a classic system design interview problem because it combines read-heavy discovery with highly concurrent, correctness-sensitive seat inventory.
The user journey looks simple:
Search Movie
↓
Choose Cinema
↓
Choose Show
↓
Select Seats
↓
Pay
↓
Get TicketThe real challenge appears when thousands of users try to reserve the same limited seats at the same time.
User A ──┐
User B ──┼──> Seat G10
User C ──┘All three users may see G10 as available, but the system must guarantee:
G10 → At most one confirmed bookingThe central design problem is:
How do we safely reserve and sell limited seat inventory under high concurrency without double booking?
This design focuses on seat reservations, concurrency, payments, high-demand traffic, real-time availability, failure handling, and scalability.
The system should support the core journey from movie discovery to confirmed booking.
Search movies by city and date.
Find cinemas and available shows.
View seat availability.
Temporarily hold selected seats.
Complete payment and confirm the booking.
Prevent double booking.
View existing bookings.
Secondary features such as cancellations, refunds, coupons, food ordering, ratings, and recommendations can be added later.
The booking path needs stronger correctness guarantees than the discovery path.
Correctness: A show-seat must never have two confirmed owners.
Availability: Movie and show discovery should remain highly available.
Low latency: Search and seat-map reads should be fast.
Scalability: Handle sudden traffic spikes for popular releases.
Durability: Confirmed bookings and payment records must survive failures.
Idempotency: Retries must not create duplicate bookings or charges.
Security: Protect user, booking, payment, and administrative data.
Discovery can tolerate slightly stale data; seat reservation and booking confirmation must prioritize correctness.
Average traffic helps with capacity planning, but ticket booking becomes difficult when a large number of users compete for the same small inventory.
Assume a hypothetical platform has:
50M daily users
5 searches per user/day
5M bookings/day
3 seats per booking
Search traffic:
50M × 5 = 250M searches/day
≈ 2,900 searches/sec average
Booking traffic:
5M / 86,400
≈ 58 bookings/sec average
The average booking rate hides the real challenge:
100,000 Users
↓
One Popular Show
↓
300 Seats
The difficult scaling dimension is not just global QPS. It is high contention over a small amount of inventory.
The data model should separate the physical seat layout from the inventory available for each show.
Core entities include:
User
Movie
Cinema
Screen
Seat
Show
ShowSeat
Reservation
Booking
BookingItem
Payment
A Seat represents a physical seat inside a screen, while ShowSeat represents that seat's inventory for a particular show.
Seat
----
seat_id
screen_id
row
number
type
For example, physical seat G10 can have different states across shows:
10:00 AM → AVAILABLE
2:00 PM → BOOKED
7:30 PM → HELD
Therefore, availability belongs to:
Show + Seat
rather than the physical seat alone.
ShowSeat
--------
show_id
seat_id
price
status
reservation_id
hold_expires_at
version
The sellable inventory unit is:
(show_id, seat_id)
A uniqueness constraint on this combination ensures that each physical seat has only one inventory record for a particular show.

A Reservation represents temporary ownership of selected seats during checkout, while a Booking represents the resulting business transaction.
Reservation
-----------
reservation_id
user_id
show_id
status
expires_at
created_at
Typical reservation states are:
ACTIVE
CONFIRMED
EXPIRED
CANCELLED
The booking stores the durable transaction:
Booking
-------
booking_id
user_id
show_id
reservation_id
amount
status
created_at
Keeping Reservation and Booking separate makes seat expiration, payment retries, and failure recovery easier to handle.
The important modeling relationship is:
Physical Seat
↓
ShowSeat
↓
Reservation
↓
Booking
This gives the booking workflow a clear separation between physical layout, show-specific inventory, temporary ownership, and confirmed purchase.
The architecture separates read-heavy discovery from the correctness-sensitive booking path.
USER APP
|
v
API GATEWAY
|
+-----------------+-----------------+
| | |
v v v
SEARCH SHOW SERVICE BOOKING SERVICE
| | |
v v v
SEARCH INDEX METADATA DB SEAT INVENTORY
|
v
RESERVATION
|
v
PAYMENT SERVICE
|
v
PAYMENT PROVIDER
RESERVATION / BOOKING EVENTS
|
v
EVENT BROKER
/ | \
v v v
REALTIME NOTIFY ANALYTICS
Search and show metadata can scale independently from seat inventory and booking transactions.
Movie and show discovery is a read-heavy workload, so it can favor low latency, caching, and scalable search.
Users may search by movie, city, cinema, language, genre, and date.
GET /v1/movies?city=lucknow&date=2026-09-18
GET /v1/movies/{movieId}/shows?city=lucknow&date=2026-09-18
At small scale, indexed relational queries may be sufficient. As search traffic and filtering requirements grow, a dedicated search index can be introduced.
Movie / Cinema DB
↓
Change Events
↓
Indexing Pipeline
↓
Search Index
↓
Search Service
Movie, cinema, and show metadata are good cache candidates because they change much less frequently than seat availability.
The search index is a derived read model. Authoritative seat ownership remains in the booking inventory.
The seat map combines the screen's mostly static physical layout with dynamic ShowSeat availability for the selected show.
GET /v1/shows/{showId}/seats
A simplified response:
{
"show_id": "SH101",
"seats": [
{
"seat_id": "G10",
"type": "PREMIUM",
"price": 350,
"status": "AVAILABLE"
}
]
}
The physical layout can be heavily cached, while show-specific availability changes frequently.
The availability shown to the user is only a point-in-time view.
User A sees:
G10 = AVAILABLE
↓
User B reserves G10
↓
User A may still see:
G10 = AVAILABLE
This temporary staleness is acceptable because the seat map does not establish ownership.
A seat becomes yours only after the authoritative reservation operation succeeds.
Double booking occurs when multiple users observe the same available seat and try to reserve it concurrently.
G10 = AVAILABLE
|
+--------+--------+
| |
v v
User A User B
| |
Read AVAILABLE Read AVAILABLE
| |
Reserve G10 Reserve G10
A naive flow is unsafe:
Read Seat
↓
Check AVAILABLE
↓
Update Seat
The read and update are separate operations, so multiple application instances can make the same decision concurrently.
The inventory layer must therefore enforce the transition atomically.

Only one concurrent request should be allowed to change a show-seat from AVAILABLE to HELD.
There are two common approaches.
A short database transaction can lock the relevant inventory row while the reservation is created.
BEGIN;
SELECT *
FROM show_seats
WHERE show_id = 'SH101'
AND seat_id = 'G10'
FOR UPDATE;
If the seat is still available, update it to HELD and commit.
This lock should exist only for the short reservation transaction. Never keep a database transaction open while the user completes a multi-minute checkout.
Instead of explicitly locking first, the system can make the state transition conditional.
UPDATE show_seats
SET status = 'HELD'
WHERE show_id = 'SH101'
AND seat_id = 'G10'
AND status = 'AVAILABLE';
Then check the affected-row count:
1 row updated → Reservation won
0 rows updated → Seat already changed
A version column can also be included when more general optimistic concurrency control is required:
UPDATE show_seats
SET status = 'HELD',
version = version + 1
WHERE show_id = 'SH101'
AND seat_id = 'G10'
AND status = 'AVAILABLE'
AND version = 7;
The implementation can vary, but the invariant cannot:
A show-seat must be acquired through one atomic authoritative operation.

A database transaction lasts milliseconds, while checkout may take several minutes. A temporary reservation bridges these two time scales.
The basic seat lifecycle is:
AVAILABLE
↓
HELD
↓
BOOKED
If checkout does not complete before the hold expires:
HELD
↓
AVAILABLE
HELD therefore represents temporary ownership, while BOOKED represents confirmed ownership.
Users often select multiple seats that must be booked together.
Suppose the request contains:
G10 G11 G12 G13
If G12 is unavailable:
Reserve 4 Seats
↓
Acquire Atomically
↓
G12 Unavailable
↓
Entire Reservation Fails
For this use case, the reservation should provide all-or-nothing semantics rather than silently reserving only part of the requested group.
Every temporary hold needs an expiration time so abandoned checkouts do not block inventory indefinitely.
reserved_at = 19:00
expires_at = 19:10
After expiration, the hold should no longer prevent another valid reservation.
A background sweeper can clean expired reservations, but correctness should not depend on it running at the exact expiration time. The authoritative reservation operation can also treat an expired hold as reclaimable when that transition is performed atomically.
ACTIVE HOLD
|
| expires
v
RECLAIMABLE
|
v
AVAILABLE / NEW HOLD
The key principle is:
Database locking protects the short atomic transition; reservation expiration manages the longer human checkout window.

Redis can provide low-latency temporary seat holds using atomic create-if-absent operations and TTL-based expiration.
hold:SH101:G10
↓
Reservation R9001
↓
TTL = 10 minutes
This can prevent multiple active holds for the same seat, but Redis alone does not solve booking correctness.
The design still needs to define what happens when a hold expires during payment, Redis becomes unavailable, or payment succeeds after expiration.
Most importantly, the durable booking layer must still enforce:
One ShowSeat
↓
At most one confirmed owner
Redis can coordinate temporary holds, but final seat ownership must be enforced by an authoritative durable store.
Explicit booking states make retries, failures, and recovery easier to handle.
A simplified lifecycle is:
CREATED
↓
SEATS_HELD
↓
PAYMENT_PENDING
↓
CONFIRMED
Failure or compensation states may include:
EXPIRED
PAYMENT_FAILED
CANCELLED
REFUND_PENDING
REFUNDED
Only valid state transitions should be allowed. For example, an EXPIRED reservation should not later become CONFIRMED without a valid inventory transition.
Payment is an external distributed workflow, so its state should be tracked independently from the booking state.
Reservation
↓
Booking Service
↓
Payment Service
↓
Payment Provider
Typical payment states are:
INITIATED
↓
PENDING
↓
SUCCEEDED
FAILED
UNKNOWN
REFUNDED
A payment request timing out does not mean the payment failed.
Payment Service
|
| Charge
v
Payment Provider
|
X Response Lost
The provider may have successfully charged the customer even though the response never reached the application.
The Payment Service should use stable payment references and idempotency and resolve uncertain outcomes through provider status APIs, webhooks, or reconciliation.
Payment timeout = unknown outcome, not automatic failure.
Never blindly retry an uncertain payment unless the provider operation is idempotent.
A critical race occurs when the seat hold expires while payment is still processing.
19:00:00 → Seat held
19:09:55 → Payment starts
19:10:00 → Hold expires
19:10:01 → User B acquires seat
19:10:03 → User A's payment succeeds
Without a clear policy, User A may pay for inventory that is no longer reserved for them.
One approach is to transition a valid reservation into a controlled PAYMENT_PENDING state before initiating payment:
ACTIVE HOLD
↓
PAYMENT_PENDING
↓
Payment Processing
↓
Verify Ownership
↓
CONFIRMED
Alternatively, final confirmation can atomically verify that the reservation still owns the seats before changing them to BOOKED.
The implementation can vary, but the invariant remains:
One ShowSeat
↓
At most one confirmed booking
Even after successful payment, booking confirmation can fail because of a crash, expired reservation, or inventory conflict.
Final confirmation should verify ownership and update the related booking state atomically where possible.
Payment Verified
↓
Booking Transaction
|
+--> Verify Reservation Ownership
|
+--> Mark ShowSeats BOOKED
|
+--> Mark Booking CONFIRMED
If the transaction succeeds:
Payment = SUCCEEDED
Booking = CONFIRMED
Seats = BOOKED
If payment succeeds but the seats cannot be confirmed:
Payment = SUCCEEDED
↓
Booking Cannot Be Confirmed
↓
Compensation Required
↓
Void / Refund
The compensation state must be durable so the workflow can recover even if the service crashes again.
Successful payment does not by itself mean the booking is confirmed. Booking confirmation requires valid seat ownership.

Network failures make retries unavoidable, so reservation, booking, payment, webhook, and event processing must be safe to repeat.
For example:
POST /v1/shows/SH101/reservations
Idempotency-Key: 52ac...
The server associates the key with the logical operation:
First Request
↓
Create Reservation R9001
Retry with Same Key
↓
Return Reservation R9001
This prevents retries from creating duplicate reservations or booking operations. Payment operations should use stable identifiers and provider-supported idempotency where available.
Webhooks and event consumers should also be idempotent because the same event may be delivered more than once.
Retries may happen multiple times, but the business effect should happen once.
Booking confirmation can trigger ticket generation, notifications, analytics, and other asynchronous work. These side effects should not make the critical booking transaction depend on every downstream service being available.
A transactional outbox prevents the database-and-broker dual-write problem.
The booking state and event are written in the same database transaction:
DB Transaction
|
+--> Booking = CONFIRMED
|
+--> Insert Outbox Event
After commit:
Outbox
↓
Publisher
↓
Event Broker
↓
Ticket / Notification / Analytics
If the broker is temporarily unavailable, the confirmed booking remains durable and the outbox publisher can retry later.
Consumers should still be idempotent because event delivery is commonly at-least-once.

Real-time updates make the seat map fresher by quickly showing when seats are held, released, or booked.
SEAT_HELD
SEAT_RELEASED
SEAT_BOOKED
↓
Event Broker
↓
Realtime Service
↓
SSE / WebSocket
↓
User Clients
SSE is a good fit when updates are primarily server-to-client. WebSockets are also reasonable when bidirectional real-time infrastructure is already available.
A client can still briefly see stale availability because another reservation may win before the update arrives.
Real-time updates improve the user experience; atomic reservation provides correctness.

A blockbuster release creates a different scaling problem because a huge number of users compete for a very small amount of inventory.
1,000,000 Users
↓
Few Popular Shows
↓
Few Thousand Seats
Simply autoscaling the Booking Service does not solve this. More application instances can generate even more concurrent requests against the same authoritative inventory.
The system therefore needs admission control before the booking path.
A virtual waiting room limits the number of users allowed to actively compete for seats.
1,000,000 Users
↓
Virtual Waiting Room
↓
Controlled Admission
↓
5,000 Active Sessions
↓
Booking Service
↓
Authoritative Inventory
Waiting users remain outside the expensive reservation path. As capacity becomes available, the system admits a controlled number of sessions.
An admitted user receives a short-lived server-issued token:
WAITING
↓
ADMITTED
↓
Admission Token
↓
Booking APIs
The booking service validates the token before accepting reservation requests.
Queue position, admission status, and expiry must remain server-controlled. The client can display this information but should not be trusted to determine it.
The waiting room controls traffic, while the business determines how users are admitted.
Possible policies include:
First come, first served.
Randomized admission when a sale opens.
Membership or presale access.
Purchase limits.
High-demand booking endpoints can also use rate limiting, account/session limits, abuse detection, and CAPTCHA or other challenges where appropriate.
Reservation endpoints should generally have stricter controls than ordinary search or seat-map reads because reservation requests directly contend for scarce inventory.
The important flow is:
Huge Demand
↓
Waiting Room
↓
Controlled Admission
↓
Atomic Reservation
↓
Authoritative Inventory
The waiting room controls how many users reach the inventory; atomic reservation decides who actually gets the seat.

Different workloads have different consistency, latency, and access-pattern requirements, so they do not need to use the same storage technology.
Workload | Suitable Storage |
|---|---|
Bookings, reservations, seat inventory | Transactional relational database |
Movie and cinema search | Search index |
Hot metadata | Distributed cache |
Posters and media | Object storage + CDN |
Asynchronous events | Durable broker/log |
Historical analytics | Analytical storage |
A relational database is a natural starting point for the booking path because seat reservations and bookings benefit from transactions, constraints, and atomic conditional updates.
The database should enforce important inventory invariants rather than relying only on application checks.
For show-specific inventory:
UNIQUE(show_id, seat_id)
ensures that each physical seat has only one authoritative inventory record for a particular show.
The schema should also prevent two confirmed bookings from permanently owning the same ShowSeat.
Application logic enforces business rules; database constraints provide a final correctness boundary.
Seat contention is naturally scoped by show_id because users booking one show do not compete with users booking another.
Show SH101
↓
Show-Specific Seat Inventory
Keeping a show's inventory together simplifies multi-seat atomic reservations, but a blockbuster show can create a hot partition.
Hundreds of Thousands of Users
↓
Show SH101
↓
Few Hundred Seats
Spreading individual seats across many partitions may distribute writes, but it also makes atomic multi-seat reservations harder.
For normal cinema seat counts, keeping transactional locality is often simpler. Exceptional demand can first be controlled through virtual queues, admission control, and rate limiting.
Sharding increases capacity, but it does not remove contention when everyone wants the same limited inventory.
Read replicas can serve less critical reads such as movie metadata and booking history.
They should not authorize seat acquisition because replication lag can expose stale availability.
Primary:
G10 = BOOKED
Replica:
G10 = AVAILABLE
The display may temporarily be stale, but the reservation operation must always use the authoritative inventory path.
Prices should be calculated from authoritative server-side data rather than trusted from the client.
Price may depend on seat category, cinema, showtime, date, taxes, fees, and promotions.
A reservation can store a price snapshot:
reservation_price
quote_expires_at
This gives the checkout flow explicit semantics for how long the quoted price remains valid.
Limited coupons create another concurrency problem. If only 100 redemptions are available, redemption should use an atomic reservation or decrement rather than:
Read Remaining
↓
Check > 0
↓
Decrement in Application
The same scarce-inventory principle used for seats applies to limited promotions.
Cancellation affects booking state, seat inventory, and potentially payment compensation.
Customer
↓
Cancel Booking
↓
Validate Cancellation Policy
↓
Booking CANCELLED
↓
Release Seats if Resale Allowed
↓
Refund Workflow
Cancellation should be idempotent so repeated requests cannot release inventory incorrectly or generate multiple refunds.
Refunds should have their own durable lifecycle:
REQUESTED
↓
PROCESSING
↓
SUCCEEDED
Possible alternative states include:
FAILED
UNKNOWN
A successful refund request to the provider is not necessarily a successful refund. SUCCEEDED should only be recorded after the provider outcome is confirmed.
The system should recover from the last durable state rather than assuming a distributed operation completely succeeded or failed.
Failure | Handling |
|---|---|
Reservation response lost | Retry with the same idempotency key |
Client disconnects after hold | Allow the reservation to expire |
Payment provider unavailable | Keep the hold only until its allowed deadline |
Payment timeout | Resolve the provider outcome before retrying or failing |
Payment succeeds, booking fails | Retry safely or start compensation/refund |
Event broker unavailable | Persist the event through the transactional outbox |
Search unavailable | Keep existing reservation and booking flows isolated |
Real-time updates unavailable | Continue using authoritative reservation state |
The recovery path can be summarized as:
Failure
↓
Read Durable State
↓
Determine Last Successful Step
↓
Retry / Resume / Compensate
Every important state transition should be durable enough that the workflow can recover after a service crash.
Discovery can be replicated across regions, but scarce seat inventory should have a clear authoritative write owner to prevent conflicting bookings.
A show can be assigned a home region:
Show
↓
Home Region
↓
Authoritative Seat Inventory
Movie and show metadata can be cached or replicated globally, while reservation and booking writes for that show are routed to its home region.
Active-active writes for the same inventory are much harder to manage:
Region A ──> G10 BOOKED
Region B ──> G10 BOOKED
If both regions accept the booking during a network partition, the system ends up with two confirmed owners for one physical seat. This conflict cannot be meaningfully merged afterward.
For scarce, non-mergeable inventory, a single authoritative write region per inventory partition is usually simpler than resolving conflicting bookings later.
Ticket platforms may support both numbered seats and capacity-based general admission, but the inventory model is different.
Model | Inventory Unit | Correctness Rule |
|---|---|---|
Numbered seats |
| One seat has at most one confirmed owner |
General admission |
| Reserved and sold quantity must not exceed capacity |
For general admission, the system atomically reserves the requested quantity:
Check Available Capacity
↓
available >= requested
↓
Reserve Atomically
The invariant is:
Reserved + Confirmed
≤
Total Capacity
For a BookMyShow-style movie ticket interview, numbered seats are the better starting point because they expose the double-booking and seat-locking problem more clearly.
Monitoring should cover both system performance and booking correctness so operational failures can be detected before they become widespread customer issues.
Important metrics include:
Reservation success and conflict rate.
Hold expiration rate.
Booking confirmation latency.
Payment success, failure, and unknown-outcome rate.
Payment-to-booking mismatch rate.
Refund failure rate.
Waiting-room size and wait time.
Database lock waits or transaction conflicts.
Hot-show inventory load.
The most important business invariant to monitor is:
Same ShowSeat
↓
More Than One Confirmed Booking
↓
Correctness Violation
The database should prevent this structurally. Monitoring acts as an additional safety signal rather than the primary protection.
Security controls should protect user accounts, booking operations, payment data, tickets, and administrative theatre APIs.
Important controls include:
Authentication and authorization.
TLS for data in transit.
Encryption at rest where appropriate.
Least-privilege access between services.
Secure secret management.
Rate limiting and abuse protection.
Audit logging for sensitive operations.
Secure payment-provider integration.
Authorization must be enforced on every sensitive booking operation:
User
↓
Booking Request
↓
Authentication
↓
Authorization
↓
Allowed Booking Operation
For example, one user must not be able to view or cancel another user's booking simply by changing a booking_id.
Ticket and QR identifiers should also be difficult to predict or forge and should be validated by the authoritative ticketing system when used.
The end-to-end booking flow connects seat discovery, temporary reservation, payment, and durable booking confirmation.
User
↓
Choose Show
↓
Load Seat Map
↓
Select Seats
↓
Reservation Service
↓
Atomic Seat Hold
↓
Reservation Created
↓
Payment Service
↓
Payment Verified
↓
Booking Service
↓
Verify Reservation Ownership
↓
Mark Seats BOOKED
↓
Booking CONFIRMED
↓
Event Broker
├──> Ticket
├──> Notification
└──> Analytics
Two operations form the main correctness boundaries:
Seat Selection
↓
Atomic Reservation
↓
Temporary Ownership
Payment Verified
↓
Atomic Confirmation
↓
Permanent Ownership
The seat map only shows a snapshot. The reservation operation determines temporary ownership, and booking confirmation determines permanent ownership.

The High-Level Design separates read-heavy discovery from the correctness-sensitive reservation and booking path.
CLIENT
|
v
API GATEWAY
|
+-----------------+-----------------+
| | |
v v v
SEARCH SHOW SERVICE BOOKING SERVICE
| | |
v v v
SEARCH INDEX METADATA DB SEAT INVENTORY
|
v
RESERVATION SERVICE
|
v
PAYMENT SERVICE
|
v
PAYMENT PROVIDER
RESERVATION / BOOKING EVENTS
|
v
EVENT BROKER
/ | \
v v v
REALTIME NOTIFY ANALYTICS
The main responsibilities are:
Component | Responsibility |
|---|---|
Search Service | Movie and cinema discovery |
Show Service | Showtimes and show metadata |
Seat Inventory | Authoritative show-seat state |
Reservation Service | Temporary seat ownership |
Booking Service | Booking lifecycle and confirmation |
Payment Service | Payment-provider integration and payment state |
Event Broker | Asynchronous event distribution |
Realtime Service | Seat-map freshness |
Notification Service | Booking and ticket notifications |
The strongest HLD deep dives are double-booking prevention, temporary seat holds, payment races, idempotency, and high-contention traffic.
The Low-Level Design focuses on domain objects, relationships, state transitions, and the concurrency rules that protect seat inventory.
Core classes include:
Movie
Cinema
Screen
Seat
Show
ShowSeat
User
Reservation
Booking
Payment
ShowSeat represents the sellable seat inventory for a particular show:
ShowSeat
--------
showId
seatId
price
status
reservationId
expiresAt
version
Reservation represents temporary ownership:
Reservation
-----------
reservationId
userId
showId
seats
expiresAt
status
reserve()
expire()
confirm()
Booking represents the durable purchase:
Booking
-------
bookingId
userId
showId
reservationId
amount
status
confirm()
cancel()
The important relationship is:
Screen
↓
Seat
↓
ShowSeat
↓
Reservation
↓
Booking
↓
Payment
For an LLD interview, simply defining classes is not enough. The important discussion is how reserve() performs an atomic inventory transition, how expiration releases temporary ownership, and how confirm() prevents an expired or invalid reservation from becoming a confirmed booking.
The data model should clearly separate physical seating, show-specific inventory, temporary reservations, and confirmed bookings.
MOVIE ──────────────┐
v
SHOW ─────────── SCREEN
| |
| v
| SEAT
| |
v |
SHOW_SEAT <─────────┘
|
v
RESERVATION
|
v
BOOKING ─────── PAYMENT
^
|
USER
The important relationships are:
Cinema 1 ─── N Screen
Screen 1 ─── N Seat
Movie 1 ─── N Show
Screen 1 ─── N Show
Show 1 ─── N ShowSeat
Seat 1 ─── N ShowSeat
Reservation 1 ─── N Reserved ShowSeats
User 1 ─── N Booking
Booking 1 ─── N Payment Attempts
ShowSeat is the important inventory entity because it represents one physical seat for one particular show.
The core seat lifecycle is:
hold expires
+--------------+
| |
v |
AVAILABLE ──> HELD -----------+
|
| booking confirmed
v
BOOKED
The reservation lifecycle can be modeled separately:
ACTIVE
|
+──> CONFIRMED
|
+──> EXPIRED
|
└──> CANCELLED
Explicit state transitions make invalid operations easier to reject. For example, an expired reservation should not be able to confirm seats unless ownership is validly reacquired through the inventory workflow.
A ticket booking system should evolve in response to measured scale, contention, and reliability requirements rather than starting with maximum architectural complexity.
MVP
Client
↓
Monolith
↓
Relational Database
Start with transactional or conditional seat updates to guarantee booking correctness.
Read Scaling
Cache
Search Index
CDN
Read Replicas
Introduce these when movie and show discovery becomes a significant read workload.
Reliable Reservations
Temporary Holds
Expiration
Idempotency
Add durable reservation state when checkout requires temporary seat ownership.
Asynchronous Workflows
Transactional Outbox
↓
Event Broker
↓
Notifications / Analytics
Move non-critical side effects away from the booking transaction.
Real-Time Seat Updates
Seat Events
↓
Realtime Service
↓
SSE / WebSocket
Add real-time delivery when seat-map freshness becomes important for the user experience.
Blockbuster Traffic
Huge Demand
↓
Virtual Waiting Room
↓
Controlled Admission
↓
Booking Service
Introduce admission control, rate limiting, and hot-show protection when extreme demand threatens the inventory path.
Regional Scale
Global Reads
↓
Show Home Region
↓
Authoritative Inventory
At regional scale, replicate discovery broadly while routing reservation and booking writes to the authoritative region for that inventory.
The important principle is:
Add architectural complexity only when it solves a specific scalability, correctness, availability, or reliability problem.
The design involves several choices where correctness, scalability, and complexity must be balanced.
Trade-Off | Consideration |
|---|---|
Pessimistic vs optimistic concurrency | Locking is straightforward for short transactions; optimistic approaches avoid long waits but can produce conflicts |
DB holds vs TTL store | DB simplifies authority; TTL stores simplify expiration but require careful coordination |
Fresh seat map vs scalability | More real-time updates improve UX but increase fan-out |
Availability vs consistency | Discovery can tolerate staleness; seat ownership cannot |
Sharding vs transaction locality | Distribution improves scale but complicates multi-seat atomicity |
Waiting room vs unrestricted traffic | Waiting adds friction but protects scarce inventory under extreme demand |
There is no single technology choice that removes the fundamental contention problem.
These questions cover the areas interviewers are most likely to explore after the initial architecture.
Use an authoritative atomic operation such as a conditional update, optimistic concurrency, or a short transactional row lock so only one reservation can acquire a show-seat.
Two requests can read AVAILABLE concurrently before either writes. The check and state transition must therefore be protected atomically.
Short row-level locks can work for the reservation transaction. Never hold the lock throughout a multi-minute user checkout.
A reservation transitions inventory from AVAILABLE to HELD with an expiration time. Payment confirmation converts the valid hold to BOOKED; expiration makes it reservable again.
It can help implement low-latency temporary holds, but the architecture still needs a durable source of truth, final ownership validation, and recovery for expiration and payment races.
Use an explicit payment-pending policy or atomically validate reservation ownership during final confirmation. Never allow two confirmed owners.
Persist the payment and booking states, retry confirmation when safe, or trigger a compensation/void/refund workflow if inventory cannot be confirmed.
Associate an idempotency key with the logical reservation or booking request and return the existing business result when the request is retried.
Acquire the requested seats atomically where practical. If one required seat cannot be acquired, reject the group rather than creating an unexpected partial reservation.
Use a virtual waiting room, controlled admission, rate limiting, cached discovery data, and protected authoritative inventory.
The bottleneck is contention over a small amount of inventory. More application instances can simply create more concurrent pressure on the same inventory.
No. It improves UX and freshness. Atomic reservation provides correctness.
A relational database is a natural starting point because reservations and bookings benefit from transactions, conditional writes, constraints, and strong invariants.
Treat cached seat availability as advisory. The final reservation must use authoritative inventory.
It ensures a committed booking can eventually produce downstream events even if the event broker is temporarily unavailable.
These mistakes usually happen when the design focuses on infrastructure but misses the correctness boundary around scarce seat inventory.
Checking seat availability and booking in separate non-atomic operations.
Holding database locks throughout the user checkout session.
Treating Redis locks as the complete double-booking solution.
Forgetting reservation expiration.
Trusting cached seat availability during reservation.
Treating a payment timeout as a confirmed payment failure.
Ignoring the race between hold expiration and payment completion.
Forgetting idempotency for reservations, payments, and booking confirmation.
Scaling or sharding infrastructure without addressing inventory contention and transaction boundaries.
The most important mistake is forgetting that seat ownership must ultimately be decided by an atomic authoritative operation.
The final architecture separates read-heavy discovery from correctness-sensitive inventory while protecting the booking path during both normal and blockbuster traffic.
USER
|
v
API GATEWAY
|
+-----------------+-----------------+
| | |
v v v
SEARCH SHOW SERVICE BOOKING SERVICE
| | |
v v v
SEARCH INDEX METADATA DB RESERVATION SERVICE
|
v
SEAT INVENTORY
|
v
PAYMENT SERVICE
|
v
PAYMENT PROVIDER
|
v
BOOKING CONFIRM
|
v
BOOKING DB
|
v
TRANSACTIONAL
OUTBOX
|
v
EVENT BROKER
/ | \
v v v
REALTIME NOTIFY ANALYTICS
For exceptional demand, admission control protects the reservation path before requests reach authoritative inventory:
High-Demand Show
1,000,000 Users
↓
Virtual Waiting Room
↓
Controlled Admission
↓
Admission Token
↓
Booking / Reservation Service
↓
Authoritative Seat Inventory
The architecture has two different optimization goals:
DISCOVERY PATH
Search / Show Metadata
↓
Cache + Search Index
↓
Fast, Scalable Reads
BOOKING PATH
Seat Selection
↓
Atomic Reservation
↓
Payment
↓
Atomic Confirmation
↓
Durable Booking
The central principle is:
Browsing tells the user what appears available; an atomic reservation determines what the user actually owns.

Search indexes, caches, replicas, and real-time updates can make discovery fast and responsive. A virtual waiting room can protect the system during extreme demand. Idempotency, durable payment states, and the transactional outbox make failures recoverable.
But when multiple users race for the final seat:
User A ──┐
├──> G10 ──> Authoritative Inventory
User B ──┘ |
v
Exactly One Winner
the authoritative inventory layer must make exactly one successful ownership decision.
That invariant—one show-seat, at most one confirmed booking—is the core correctness guarantee of a scalable BookMyShow-like ticket booking system.