Intermediate · 9 min read · updated 14 November 2026
Quick answer
How do you design a movie ticket booking system like BookMyShow in an LLD interview?
Model City, Cinema, Screen, Seat, Show, ShowSeat, Booking, Payment and User, with seat status on ShowSeat because a seat is free for one show and sold for another. Booking holds the chosen ShowSeats for a few minutes, takes payment, then marks them booked or releases them. A version check or row lock makes sure two buyers can never book the same seat.
A booking system sells a fixed, contested resource: one seat, one show, one buyer. The design interview is about the model and, above all, about the race when two people click the same seat.
Entities and the class diagram
City → Cinema → Screen → Seat: the physical side, rarely changes.
Show: one movie, one start time, one screen.
ShowSeat: one seat for one show, holding status, version and hold expiry.
Booking: a user's ShowSeats for a show, the amount and a status.
Payment: belongs to one booking; it decides confirm or release.
The booking model, one class at a time
Start from the thing people actually buy into: a Show is one movie at one start time on one screen.
0:01 / 0:25Step 1 of 9
Transcript · 9 steps
The booking flow
Read the show's seat map (ShowSeats, not Seats).
Hold the chosen seats in one short transaction, all or none, for a fixed time.
Charge the payment gateway outside any lock.
Success: mark the seats BOOKED and confirm. Failure: release them at once.
No answer in time: the hold lapses on its own.
Booking two seats: hold, pay, confirm
Bob opens the 18:00 show. The service reads the ShowSeats of that show, not the screen's Seats, because only ShowSeat knows whether a seat is free tonight.
0:01 / 0:27Step 1 of 9
Transcript · 9 steps
Concurrency: two buyers, one seat
Reading "A2 is free" proves nothing: the other buyer read it too.
Optimistic lock: UPDATE show_seat SET status = 'HELD', version = version + 1 WHERE id = ? AND version = ?. Zero rows changed means you lost.
Pessimistic lock: SELECT … FOR UPDATE on the seat rows, then write, then commit. Simple, but keep it short.
A unique constraint on (show, seat) in a bookings table is a third guard.
Never rely on an in-memory lock: you run more than one server.
Two buyers, one seat: the version check lets one win
Alice and bob both want A2 for the same show. The ShowSeat row says AVAILABLE, version 0. Without a guard, both could see it free and both could book it.
0:01 / 0:16Step 1 of 5
Transcript · 5 steps
Seat status as a state machine
AVAILABLE → hold → HELD → pay ok → BOOKED.
HELD → payment fails or time runs out → AVAILABLE.
BOOKED refuses every hold; a cancellation is its own transition.
This is the State pattern: each state decides what each action may do.
A seat's status is a state machine: AVAILABLE, HELD, BOOKED
Every ShowSeat starts AVAILABLE. The status is a small state machine: each state decides what hold, pay and time are allowed to do to it.
0:01 / 0:21Step 1 of 8
Transcript · 8 steps
Patterns that fit
Strategy for pricing: price(seat, show) swaps weekday, weekend or premium-row rules.
Observer for notifications: mail, SMS and analytics subscribe to booking events.
A version check and an expiring hold, then the race, a declined card and a lapsed hold.
C++JavaPythonJavaScript
Copy
#include<functional>#include<iostream>#include<map>#include<string>#include<vector>usingnamespace std;constint HOLD =5;// minutes a seat hold lasts before it lapsesstructShowSeat{// one seat for one show: the row two buyers race for
string status ="AVAILABLE";int version =0, by =0, until =0;
string state(int now)const{// a lapsed hold reads as free: no cleanup job neededreturn status =="HELD"&& now >= until ?"AVAILABLE": status;}voidwrite(const string& s,int b =0,int u =0){ status = s; by = b; until = u; version++;}// every write bumps the version};structBooking{int id; string user; vector<string> seats;int amount;};
string join(const vector<string>& v){
string r;for(size_t i =0; i < v.size(); i++) r +=(i ?" ":"")+ v[i];return r;}classBookingService{public:
map<string, ShowSeat> seats;// ordered by seat id
function<int(const string&)> pricing;// a Strategy: swap in weekend or dynamic pricingint nextId =1;BookingService(vector<string> ids, function<int(const string&)> p):pricing(p){for(auto& s : ids) seats[s]=ShowSeat();}
vector<int>read(const vector<string>& ids){
vector<int> v;for(auto& s : ids) v.push_back(seats[s].version);return v;}
Booking*hold(const string& user,const vector<string>& ids,const vector<int>& seen,int now){// all seats or none
string head ="t="+to_string(now)+" "+ user +" holds "+join(ids)+": ";for(size_t i =0; i < ids.size(); i++){
ShowSeat& seat = seats[ids[i]];if(seat.version != seen[i]){// UPDATE ... WHERE version = seen matched 0 rows
cout << head <<"FAILED, "<< ids[i]<<" is at version "<< seat.version <<", expected "<< seen[i]<<"\n";returnnullptr;}if(seat.state(now)!="AVAILABLE"){ cout << head <<"FAILED, "<< ids[i]<<" is taken\n";returnnullptr;}}int amount =0;for(auto& s : ids) amount +=pricing(s);
Booking* b =new Booking{nextId++, user, ids, amount};for(auto& s : ids) seats[s].write("HELD", b->id, now + HOLD);
cout << head <<"OK, booking "<< b->id <<" held until t="<< now + HOLD <<"\n";return b;}voidpay(Booking* b,bool paid,int now){
vector<string> mine;for(auto& s : b->seats)if(seats[s].by == b->id && seats[s].state(now)=="HELD") mine.push_back(s);
cout <<"t="<< now <<" "<< b->user <<" pays "<< b->amount <<": ";if(paid && mine.size()== b->seats.size()){for(auto& s : mine) seats[s].write("BOOKED", b->id);
cout <<"CONFIRMED booking "<< b->id <<"\n";}else{for(auto& s : mine) seats[s].write("AVAILABLE");
cout <<"FAILED, released "<<join(mine)<<"\n";}}};intmain(){
BookingService svc({"A1","A2","B1"},[](const string& s){return s[0]=='A'?250:180;});// front row A costs more
vector<int> seenA = svc.read({"A1","A2"}), seenB = svc.read({"A2","B1"});// both read before either writes
cout <<"alice and bob both read A2 at version "<< seenA[1]<<"\n";
Booking* alice = svc.hold("alice",{"A1","A2"}, seenA,0);
svc.hold("bob",{"A2","B1"}, seenB,0);
svc.pay(alice,false,2);// the card is declined
Booking* bob = svc.hold("bob",{"A2","B1"}, svc.read({"A2","B1"}),3);
svc.pay(bob,true,4);
svc.hold("carol",{"A1"}, svc.read({"A1"}),6);// carol never pays
svc.hold("dave",{"A1"}, svc.read({"A1"}),20);// her hold lapsed at t=11
cout <<"t=20 seats:";for(auto&[id, s]: svc.seats) cout <<" "<< id <<"="<< s.state(20);
cout <<"\n";}
Output
alice and bob both read A2 at version 0
t=0 alice holds A1 A2: OK, booking 1 held until t=5
t=0 bob holds A2 B1: FAILED, A2 is at version 1, expected 0
t=2 alice pays 500: FAILED, released A1 A2
t=3 bob holds A2 B1: OK, booking 2 held until t=8
t=4 bob pays 430: CONFIRMED booking 2
t=6 carol holds A1: OK, booking 3 held until t=11
t=20 dave holds A1: OK, booking 4 held until t=25
t=20 seats: A1=HELD A2=BOOKED B1=BOOKED
Interview questions
Where does seat status live? On ShowSeat, one row per seat per show; Seat stays physical.
How do you prevent double booking? A conditional write (version check) or SELECT … FOR UPDATE, decided by the database, plus a unique constraint as a backstop.
Why hold before paying? So nobody pays for a seat that is already gone; the hold's expiry frees seats from abandoned checkouts.
What if payment succeeds after the hold lapsed? Confirm only if the seats are still held by this booking; otherwise refund automatically.
How would you scale a blockbuster release? Shard by show, cache the seat map briefly, and keep the hold write as the one strongly consistent step.
Why do we need a ShowSeat class instead of putting status on Seat?
A Seat is physical and belongs to a screen for years, but it is free at the 6 pm show and sold at the 9 pm one. Status, price, version and hold expiry depend on the show, so they live on ShowSeat, one row per seat per show.
How do you stop two users from booking the same seat?
Make the write conditional. With optimistic locking each ShowSeat has a version and the hold is UPDATE … WHERE version = the one read, so only the first write changes a row. With pessimistic locking you SELECT … FOR UPDATE the rows inside a short transaction. Either way the database decides, not application code.
What is a seat hold and why does it expire?
A hold reserves seats for a few minutes while the user pays. Without expiry, a user who closes the tab would block the seats forever. Storing the expiry time on the seat lets a lapsed hold simply read as available, with a background sweep as a tidy-up only.
Should payment happen inside the database transaction?
No. A payment gateway can take seconds or time out, and holding row locks that long blocks other buyers. Hold the seats in one short transaction, call the gateway outside it, then confirm or release in another short transaction.
Which design patterns fit a booking system?
Strategy for pricing (weekday, weekend, premium rows, dynamic), Observer for notifications when a booking is confirmed or cancelled, State for the seat and booking lifecycles, and often a Singleton or dependency-injected BookingService as the one entry point.
Low-level design of a movie ticket booking system like BookMyShow: classes, seat holds with a timeout, the double-booking race and patterns, with code.
How do you design a movie ticket booking system like BookMyShow in an LLD interview?
Model City, Cinema, Screen, Seat, Show, ShowSeat, Booking, Payment and User, with seat status on ShowSeat because a seat is free for one show and sold for another. Booking holds the chosen ShowSeats for a few minutes, takes payment, then marks them booked or releases them. A version check or row lock makes sure two buyers can never book the same seat.
A booking system sells a fixed, contested resource: one seat, one show, one buyer. The design interview is about the model and, above all, about the race when two people click the same seat.
Entities and the class diagram
City → Cinema → Screen → Seat: the physical side, rarely changes.
Show: one movie, one start time, one screen.
ShowSeat: one seat for one show, holding status, version and hold expiry.
Booking: a user's ShowSeats for a show, the amount and a status.
Payment: belongs to one booking; it decides confirm or release.
The booking model, one class at a time.
Start from the thing people actually buy into: a Show is one movie at one start time on one screen.
A Show plays on a Screen. Many shows reuse the same screen across the day, so the show only points at it.
A Screen is made of physical Seats (row, number, type). They exist as long as the screen does, hence composition.
A Cinema owns its screens. Deleting a cinema deletes its screens, so this is composition too.
A City groups cinemas for browsing. A cinema is not part of a city the way a seat is part of a screen: aggregation.
The key class: ShowSeat is one seat for one show. A seat is free at 6 pm and sold at 9 pm, so status, version and the hold's expiry live here, not on Seat.
A Booking ties a show to the ShowSeats it holds, with an amount from the pricing strategy and its own status.
A User makes bookings. Keep the user thin: login, profile and history belong to other services.
A Payment belongs to one booking. Its success confirms the seats; its failure releases them. That is the whole model.
The booking flow
Read the show's seat map (ShowSeats, not Seats).
Hold the chosen seats in one short transaction, all or none, for a fixed time.
Charge the payment gateway outside any lock.
Success: mark the seats BOOKED and confirm. Failure: release them at once.
No answer in time: the hold lapses on its own.
Booking two seats: hold, pay, confirm.
Bob opens the 18:00 show. The service reads the ShowSeats of that show, not the screen's Seats, because only ShowSeat knows whether a seat is free tonight.
At t=3 alice's failed payment has released her seats, so A1, A2, B1 are all free. Bob picks A2 and B1.
Bob asks to hold both seats. The request carries the versions bob read, so a seat that changed since will refuse the write.
The service writes both seats in one transaction, each only if its version is still the one bob read (2 and 0). All match, so both become HELD.
The hold lasts 5 minutes, until t=8. Nobody else can hold A2 or B1 until bob pays, gives up or the time runs out.
The pricing strategy priced row A at 250 and row B at 180, so the booking asks the payment gateway for 430.
The gateway answers success at t=4, inside the hold window. Payment happens outside any database lock: it can take seconds.
Only now do the seats become BOOKED, and only the seats this booking holds. Had the card failed, the same call would set them back to AVAILABLE.
Bob gets booking 2. An observer on the booking would now send the ticket by mail and SMS without the service knowing how.
Concurrency: two buyers, one seat
Reading "A2 is free" proves nothing: the other buyer read it too.
Optimistic lock: UPDATE show_seat SET status = 'HELD', version = version + 1 WHERE id = ? AND version = ?. Zero rows changed means you lost.
Pessimistic lock: SELECT … FOR UPDATE on the seat rows, then write, then commit. Simple, but keep it short.
A unique constraint on (show, seat) in a bookings table is a third guard.
Never rely on an in-memory lock: you run more than one server.
Two buyers, one seat: the version check lets one win.
Alice and bob both want A2 for the same show. The ShowSeat row says AVAILABLE, version 0. Without a guard, both could see it free and both could book it.
Both read the seat before either writes: each now holds "A2 is free at version 0". This is the race: the reads cannot tell who will win.
Alice's write goes first: set HELD where version = 0. The row still has version 0, so one row changes and the version becomes 1.
Bob's write says the same thing, where version = 0, but the row is now at version 1. Zero rows change: the database itself refused the second booking.
Exactly 1 of the two holds landed. Bob is told A2 was just taken and picks another seat; alice has 5 minutes to pay before her hold lapses.
Seat status as a state machine
AVAILABLE → hold → HELD → pay ok → BOOKED.
HELD → payment fails or time runs out → AVAILABLE.
BOOKED refuses every hold; a cancellation is its own transition.
This is the State pattern: each state decides what each action may do.
A seat's status is a state machine: AVAILABLE, HELD, BOOKED.
Every ShowSeat starts AVAILABLE. The status is a small state machine: each state decides what hold, pay and time are allowed to do to it.
t=0: alice holds A1 and A2. AVAILABLE allows a hold, so both move to HELD, each with an expiry 5 minutes away.
t=2: alice's card is declined. HELD allows a release, so both seats go straight back to AVAILABLE; nobody waits for the timer.
t=3: bob holds A2 and B1. A2 is held for the second time tonight; B1 for the first.
t=4: bob's payment succeeds while his seats are still HELD, so A2 and B1 become BOOKED. BOOKED refuses every further hold.
t=6: carol holds A1 and then never pays. The hold runs until t=11.
t=11: carol's hold has lapsed. Nobody had to run a job: a HELD seat past its expiry simply reads as AVAILABLE.
t=20: dave holds A1 and gets it. Final state: A1 HELD for dave, A2 and B1 BOOKED for bob.
Patterns that fit
Strategy for pricing: price(seat, show) swaps weekday, weekend or premium-row rules.
Observer for notifications: mail, SMS and analytics subscribe to booking events.
A version check and an expiring hold, then the race, a declined card and a lapsed hold.
#include <functional>
#include <iostream>
#include <map>
#include <string>
#include <vector>
using namespace std;
const int HOLD = 5; // minutes a seat hold lasts before it lapses
struct ShowSeat { // one seat for one show: the row two buyers race for
string status = "AVAILABLE";
int version = 0, by = 0, until = 0;
string state(int now) const { // a lapsed hold reads as free: no cleanup job needed
return status == "HELD" && now >= until ? "AVAILABLE" : status;
}
void write(const string& s, int b = 0, int u = 0) { status = s; by = b; until = u; version++; } // every write bumps the version
};
struct Booking { int id; string user; vector<string> seats; int amount; };
string join(const vector<string>& v) {
string r;
for (size_t i = 0; i < v.size(); i++) r += (i ? " " : "") + v[i];
return r;
}
class BookingService {
public:
map<string, ShowSeat> seats; // ordered by seat id
function<int(const string&)> pricing; // a Strategy: swap in weekend or dynamic pricing
int nextId = 1;
BookingService(vector<string> ids, function<int(const string&)> p) : pricing(p) {
for (auto& s : ids) seats[s] = ShowSeat();
}
vector<int> read(const vector<string>& ids) {
vector<int> v;
for (auto& s : ids) v.push_back(seats[s].version);
return v;
}
Booking* hold(const string& user, const vector<string>& ids, const vector<int>& seen, int now) { // all seats or none
string head = "t=" + to_string(now) + " " + user + " holds " + join(ids) + ": ";
for (size_t i = 0; i < ids.size(); i++) {
ShowSeat& seat = seats[ids[i]];
if (seat.version != seen[i]) { // UPDATE ... WHERE version = seen matched 0 rows
cout << head << "FAILED, " << ids[i] << " is at version " << seat.version << ", expected " << seen[i] << "\n";
return nullptr;
}
if (seat.state(now) != "AVAILABLE") { cout << head << "FAILED, " << ids[i] << " is taken\n"; return nullptr; }
}
int amount = 0;
for (auto& s : ids) amount += pricing(s);
Booking* b = new Booking{nextId++, user, ids, amount};
for (auto& s : ids) seats[s].write("HELD", b->id, now + HOLD);
cout << head << "OK, booking " << b->id << " held until t=" << now + HOLD << "\n";
return b;
}
void pay(Booking* b, bool paid, int now) {
vector<string> mine;
for (auto& s : b->seats) if (seats[s].by == b->id && seats[s].state(now) == "HELD") mine.push_back(s);
cout << "t=" << now << " " << b->user << " pays " << b->amount << ": ";
if (paid && mine.size() == b->seats.size()) {
for (auto& s : mine) seats[s].write("BOOKED", b->id);
cout << "CONFIRMED booking " << b->id << "\n";
} else {
for (auto& s : mine) seats[s].write("AVAILABLE");
cout << "FAILED, released " << join(mine) << "\n";
}
}
};
int main() {
BookingService svc({"A1", "A2", "B1"}, [](const string& s) { return s[0] == 'A' ? 250 : 180; }); // front row A costs more
vector<int> seenA = svc.read({"A1", "A2"}), seenB = svc.read({"A2", "B1"}); // both read before either writes
cout << "alice and bob both read A2 at version " << seenA[1] << "\n";
Booking* alice = svc.hold("alice", {"A1", "A2"}, seenA, 0);
svc.hold("bob", {"A2", "B1"}, seenB, 0);
svc.pay(alice, false, 2); // the card is declined
Booking* bob = svc.hold("bob", {"A2", "B1"}, svc.read({"A2", "B1"}), 3);
svc.pay(bob, true, 4);
svc.hold("carol", {"A1"}, svc.read({"A1"}), 6); // carol never pays
svc.hold("dave", {"A1"}, svc.read({"A1"}), 20); // her hold lapsed at t=11
cout << "t=20 seats:";
for (auto& [id, s] : svc.seats) cout << " " << id << "=" << s.state(20);
cout << "\n";
}
import java.util.*;
import java.util.function.ToIntFunction;
public class Main {
static final int HOLD = 5; // minutes a seat hold lasts before it lapses
static class ShowSeat { // one seat for one show: the row two buyers race for
final String id;
String status = "AVAILABLE";
int version = 0, by = 0, until = 0;
ShowSeat(String id) { this.id = id; }
String state(int now) { // a lapsed hold reads as free: no cleanup job needed
return status.equals("HELD") && now >= until ? "AVAILABLE" : status;
}
void write(String s, int b, int u) { status = s; by = b; until = u; version++; } // every write bumps the version
}
record Booking(int id, String user, List<String> seats, int amount) {}
static class BookingService {
final Map<String, ShowSeat> seats = new TreeMap<>(); // ordered by seat id
final ToIntFunction<String> pricing; // a Strategy: swap in weekend or dynamic pricing
int nextId = 1;
BookingService(List<String> ids, ToIntFunction<String> pricing) {
for (String s : ids) seats.put(s, new ShowSeat(s));
this.pricing = pricing;
}
List<Integer> read(List<String> ids) {
List<Integer> v = new ArrayList<>();
for (String s : ids) v.add(seats.get(s).version);
return v;
}
Booking hold(String user, List<String> ids, List<Integer> seen, int now) { // all seats or none
String head = "t=" + now + " " + user + " holds " + String.join(" ", ids) + ": ";
for (int i = 0; i < ids.size(); i++) {
ShowSeat seat = seats.get(ids.get(i));
if (seat.version != seen.get(i)) { // UPDATE ... WHERE version = seen matched 0 rows
System.out.println(head + "FAILED, " + seat.id + " is at version " + seat.version + ", expected " + seen.get(i));
return null;
}
if (!seat.state(now).equals("AVAILABLE")) {
System.out.println(head + "FAILED, " + seat.id + " is taken");
return null;
}
}
int amount = 0;
for (String s : ids) amount += pricing.applyAsInt(s);
Booking b = new Booking(nextId++, user, ids, amount);
for (String s : ids) seats.get(s).write("HELD", b.id(), now + HOLD);
System.out.println(head + "OK, booking " + b.id() + " held until t=" + (now + HOLD));
return b;
}
void pay(Booking b, boolean paid, int now) {
List<String> mine = new ArrayList<>();
for (String s : b.seats()) if (seats.get(s).by == b.id() && seats.get(s).state(now).equals("HELD")) mine.add(s);
String head = "t=" + now + " " + b.user() + " pays " + b.amount() + ": ";
if (paid && mine.size() == b.seats().size()) {
for (String s : mine) seats.get(s).write("BOOKED", b.id(), 0);
System.out.println(head + "CONFIRMED booking " + b.id());
} else {
for (String s : mine) seats.get(s).write("AVAILABLE", 0, 0);
System.out.println(head + "FAILED, released " + String.join(" ", mine));
}
}
}
public static void main(String[] args) {
BookingService svc = new BookingService(List.of("A1", "A2", "B1"), s -> s.startsWith("A") ? 250 : 180); // front row A costs more
List<Integer> seenA = svc.read(List.of("A1", "A2")), seenB = svc.read(List.of("A2", "B1")); // both read before either writes
System.out.println("alice and bob both read A2 at version " + seenA.get(1));
Booking alice = svc.hold("alice", List.of("A1", "A2"), seenA, 0);
svc.hold("bob", List.of("A2", "B1"), seenB, 0);
svc.pay(alice, false, 2); // the card is declined
Booking bob = svc.hold("bob", List.of("A2", "B1"), svc.read(List.of("A2", "B1")), 3);
svc.pay(bob, true, 4);
svc.hold("carol", List.of("A1"), svc.read(List.of("A1")), 6); // carol never pays
svc.hold("dave", List.of("A1"), svc.read(List.of("A1")), 20); // her hold lapsed at t=11
StringBuilder out = new StringBuilder("t=20 seats:");
for (ShowSeat s : svc.seats.values()) out.append(" ").append(s.id).append("=").append(s.state(20));
System.out.println(out);
}
}
HOLD = 5 # minutes a seat hold lasts before it lapses
class ShowSeat: # one seat for one show: the row two buyers race for
def __init__(self, sid):
self.id, self.status, self.version, self.by, self.until = sid, "AVAILABLE", 0, 0, 0
def state(self, now): # a lapsed hold reads as free: no cleanup job needed
return "AVAILABLE" if self.status == "HELD" and now >= self.until else self.status
def write(self, status, by=0, until=0): # every write bumps the version
self.status, self.by, self.until = status, by, until
self.version += 1
class Booking:
def __init__(self, bid, user, seats, amount):
self.id, self.user, self.seats, self.amount = bid, user, seats, amount
class BookingService:
def __init__(self, seat_ids, pricing):
self.seats = {s: ShowSeat(s) for s in seat_ids}
self.pricing, self.next_id = pricing, 1 # pricing is a Strategy
def read(self, ids):
return [self.seats[s].version for s in ids]
def hold(self, user, ids, seen, now): # all seats or none, like one transaction
head = f"t={now} {user} holds {' '.join(ids)}: "
for s, v in zip(ids, seen):
seat = self.seats[s]
if seat.version != v: # UPDATE ... WHERE version = seen matched 0 rows
print(f"{head}FAILED, {s} is at version {seat.version}, expected {v}")
return None
if seat.state(now) != "AVAILABLE":
print(f"{head}FAILED, {s} is taken")
return None
b = Booking(self.next_id, user, ids, sum(self.pricing(s) for s in ids))
self.next_id += 1
for s in ids:
self.seats[s].write("HELD", b.id, now + HOLD)
print(f"{head}OK, booking {b.id} held until t={now + HOLD}")
return b
def pay(self, b, paid, now):
mine = [s for s in b.seats if self.seats[s].by == b.id and self.seats[s].state(now) == "HELD"]
head = f"t={now} {b.user} pays {b.amount}: "
if paid and len(mine) == len(b.seats):
for s in mine:
self.seats[s].write("BOOKED", b.id)
print(f"{head}CONFIRMED booking {b.id}")
else:
for s in mine:
self.seats[s].write("AVAILABLE")
print(f"{head}FAILED, released {' '.join(mine)}")
def row_pricing(seat): # front row A costs more
return 250 if seat.startswith("A") else 180
svc = BookingService(["A1", "A2", "B1"], row_pricing)
seen_a, seen_b = svc.read(["A1", "A2"]), svc.read(["A2", "B1"]) # both read before either writes
print(f"alice and bob both read A2 at version {seen_a[1]}")
alice = svc.hold("alice", ["A1", "A2"], seen_a, 0)
svc.hold("bob", ["A2", "B1"], seen_b, 0)
svc.pay(alice, False, 2) # the card is declined
bob = svc.hold("bob", ["A2", "B1"], svc.read(["A2", "B1"]), 3)
svc.pay(bob, True, 4)
svc.hold("carol", ["A1"], svc.read(["A1"]), 6) # carol never pays
svc.hold("dave", ["A1"], svc.read(["A1"]), 20) # her hold lapsed at t=11
print("t=20 seats: " + " ".join(f"{s}={svc.seats[s].state(20)}" for s in sorted(svc.seats)))
const HOLD = 5; // minutes a seat hold lasts before it lapses
class ShowSeat { // one seat for one show: the row two buyers race for
constructor(id) {
this.id = id;
this.status = "AVAILABLE";
this.version = 0;
this.by = 0;
this.until = 0;
}
state(now) { // a lapsed hold reads as free: no cleanup job needed
return this.status === "HELD" && now >= this.until ? "AVAILABLE" : this.status;
}
write(status, by = 0, until = 0) { // every write bumps the version
this.status = status;
this.by = by;
this.until = until;
this.version++;
}
}
class BookingService {
constructor(ids, pricing) {
this.seats = new Map(ids.map((s) => [s, new ShowSeat(s)]));
this.pricing = pricing; // a Strategy: swap in weekend or dynamic pricing
this.nextId = 1;
}
read(ids) { return ids.map((s) => this.seats.get(s).version); }
hold(user, ids, seen, now) { // all seats or none, like one transaction
const head = `t=${now} ${user} holds ${ids.join(" ")}: `;
for (let i = 0; i < ids.length; i++) {
const seat = this.seats.get(ids[i]);
if (seat.version !== seen[i]) { // UPDATE ... WHERE version = seen matched 0 rows
console.log(`${head}FAILED, ${ids[i]} is at version ${seat.version}, expected ${seen[i]}`);
return null;
}
if (seat.state(now) !== "AVAILABLE") {
console.log(`${head}FAILED, ${ids[i]} is taken`);
return null;
}
}
const b = { id: this.nextId++, user, seats: ids, amount: ids.reduce((sum, s) => sum + this.pricing(s), 0) };
for (const s of ids) this.seats.get(s).write("HELD", b.id, now + HOLD);
console.log(`${head}OK, booking ${b.id} held until t=${now + HOLD}`);
return b;
}
pay(b, paid, now) {
const mine = b.seats.filter((s) => this.seats.get(s).by === b.id && this.seats.get(s).state(now) === "HELD");
const head = `t=${now} ${b.user} pays ${b.amount}: `;
if (paid && mine.length === b.seats.length) {
for (const s of mine) this.seats.get(s).write("BOOKED", b.id);
console.log(`${head}CONFIRMED booking ${b.id}`);
} else {
for (const s of mine) this.seats.get(s).write("AVAILABLE");
console.log(`${head}FAILED, released ${mine.join(" ")}`);
}
}
}
const svc = new BookingService(["A1", "A2", "B1"], (s) => (s.startsWith("A") ? 250 : 180)); // front row A costs more
const seenA = svc.read(["A1", "A2"]);
const seenB = svc.read(["A2", "B1"]); // both read before either writes
console.log(`alice and bob both read A2 at version ${seenA[1]}`);
const alice = svc.hold("alice", ["A1", "A2"], seenA, 0);
svc.hold("bob", ["A2", "B1"], seenB, 0);
svc.pay(alice, false, 2); // the card is declined
const bob = svc.hold("bob", ["A2", "B1"], svc.read(["A2", "B1"]), 3);
svc.pay(bob, true, 4);
svc.hold("carol", ["A1"], svc.read(["A1"]), 6); // carol never pays
svc.hold("dave", ["A1"], svc.read(["A1"]), 20); // her hold lapsed at t=11
console.log("t=20 seats: " + [...svc.seats.keys()].sort().map((s) => `${s}=${svc.seats.get(s).state(20)}`).join(" "));
alice and bob both read A2 at version 0
t=0 alice holds A1 A2: OK, booking 1 held until t=5
t=0 bob holds A2 B1: FAILED, A2 is at version 1, expected 0
t=2 alice pays 500: FAILED, released A1 A2
t=3 bob holds A2 B1: OK, booking 2 held until t=8
t=4 bob pays 430: CONFIRMED booking 2
t=6 carol holds A1: OK, booking 3 held until t=11
t=20 dave holds A1: OK, booking 4 held until t=25
t=20 seats: A1=HELD A2=BOOKED B1=BOOKED
Interview questions
Where does seat status live? On ShowSeat, one row per seat per show; Seat stays physical.
How do you prevent double booking? A conditional write (version check) or SELECT … FOR UPDATE, decided by the database, plus a unique constraint as a backstop.
Why hold before paying? So nobody pays for a seat that is already gone; the hold's expiry frees seats from abandoned checkouts.
What if payment succeeds after the hold lapsed? Confirm only if the seats are still held by this booking; otherwise refund automatically.
How would you scale a blockbuster release? Shard by show, cache the seat map briefly, and keep the hold write as the one strongly consistent step.
Why do we need a ShowSeat class instead of putting status on Seat?
A Seat is physical and belongs to a screen for years, but it is free at the 6 pm show and sold at the 9 pm one. Status, price, version and hold expiry depend on the show, so they live on ShowSeat, one row per seat per show.
How do you stop two users from booking the same seat?
Make the write conditional. With optimistic locking each ShowSeat has a version and the hold is UPDATE … WHERE version = the one read, so only the first write changes a row. With pessimistic locking you SELECT … FOR UPDATE the rows inside a short transaction. Either way the database decides, not application code.
What is a seat hold and why does it expire?
A hold reserves seats for a few minutes while the user pays. Without expiry, a user who closes the tab would block the seats forever. Storing the expiry time on the seat lets a lapsed hold simply read as available, with a background sweep as a tidy-up only.
Should payment happen inside the database transaction?
No. A payment gateway can take seconds or time out, and holding row locks that long blocks other buyers. Hold the seats in one short transaction, call the gateway outside it, then confirm or release in another short transaction.
Which design patterns fit a booking system?
Strategy for pricing (weekday, weekend, premium rows, dynamic), Observer for notifications when a booking is confirmed or cancelled, State for the seat and booking lifecycles, and often a Singleton or dependency-injected BookingService as the one entry point.