Practice · Low Level Design · Shared State and Contention
Design a Flight Booking System
Two people, one seat, the same second: holding seats for ten minutes, giving them back when nobody pays, and never selling a seat twice.
Learn mode: the full solution is shown below.
Problem introduction
| Difficulty | Intermediate |
| Time | 2–3 hours to design and code; about 45 minutes in an interview |
| Prerequisites | Interfaces, maps, locks |
| Concepts | Seat states (free, held, booked), holds that expire, an injected clock, all-or-nothing holds, a pricing Strategy wrapped by a Decorator, one lock for check-and-change |
| Real world | Airline and bus ticket sites, cinema and concert seats, train tickets on Eid, any "pick a seat, then pay" flow |
Flight booking is the classic "shared resource" question. Choosing a seat and paying are two steps with minutes in between, and thousands of people are looking at the same seat map. The design has to answer three things: how a seat is kept for you while you pay, what happens when you never pay, and how two people clicking the same seat in the same millisecond can't both get it.
Problem statement
Flight CF-141 flies Dhaka to Cox's Bazar. It has 2 business seats (row 1) and 6 economy seats (rows 2 and 3). Before Eid, everyone opens the app at once.
A passenger picks one or more seats. The seats are held for 10 minutes so nobody else can take them while the passenger pays. If they pay in time, the seats are booked and they get a PNR. If they don't pay, or they close the app, the seats go back on sale.
What we are building: the booking core for one flight: the seat map, holds, payment, releasing seats, and prices.
Why it matters: if two people get the same seat, one of them is standing at the gate with a ticket and no seat. If abandoned holds never come back, a full-looking flight takes off half empty.
In one line: let one passenger hold seats for a short time, turn the hold into a booking when they pay, give the seats back when they don't, and never sell a seat twice.
Requirements
Functional requirements
- A flight has seats; each seat has a number (2A) and a class (business, economy).
- Hold(passenger, seats): keep all the chosen seats for 10 minutes, or none of them if any is taken.
- Pay(hold): turn a live hold into a booking with a PNR. A hold whose time is up is refused.
- Release(hold): give the seats back at once (the passenger gave up).
- When a hold's time is up, its seats become free again without anyone doing anything.
- Price each seat by its class; the last free seat of a class costs 25% more.
- Refuse seats that don't exist, and the same seat picked twice.
Non-functional requirements
| Requirement | What it means here |
|---|---|
| Never double-sell | Two people holding the same seat at the same moment: exactly one wins |
| Seats come back | An unpaid hold frees its seats on time, so they can be sold again |
| Extensibility | A new pricing rule (Eid surge, student discount) doesn't touch booking code |
| Testability | Time comes from a clock we pass in, so a test can jump 10 minutes without waiting |
Assumptions and out of scope
- One flight, one currency (taka). Payment is one call that always succeeds; no real gateway, no refunds, no cancelling a booking, no passenger details.
What to hand in
- Entities, the seat state diagram, and where the lock is.
- Code for hold, pay, release and expiry.
- Tests for all-or-nothing holds, expiry, pricing and concurrency.
Hint 1: a seat's life
Write down the states a seat can be in, and what moves it from one to the next. There's a way back from one of them.
Hint 2: two seats, one taken
If someone holds 2A and 2B but 2B is taken, should they keep 2A? Families want seats together. Check every seat before changing any.
Hint 3: who frees an expired hold?
You could run a background timer, or you could check for expired holds at the start of every operation. The second one needs no extra goroutine.
Hint 4: the same millisecond
"Is it free?" and "mark it held" must happen as one step. If another goroutine can run between them, both see "free".
Core entities
| Entity | Job in one sentence | Holds | Does |
|---|---|---|---|
Seat | One seat and where it is in its life | number, class, state, which hold has it | nothing: data |
State | A seat's state | free / held / booked | nothing: an enum |
Hold | Seats kept for one passenger until a deadline | id, passenger, seats, total, expires | nothing: a value |
Booking | A paid reservation | PNR, passenger, seats, total | nothing: a value |
Fare (interface) | Prices one seat | depends on the rule | Price(class, left) |
ClassFare, Surge | A fixed price per class; a wrapper that adds a percentage for the last seats | prices; base fare, threshold, percent | Price |
Clock (interface) | Tells the time | nothing | Now() |
Flight | The entry point; owns seats, holds and the lock | seats in map order, live holds, expired holds, counters | Hold, Pay, Release, Seats |
Relationships: Flight owns its seats and holds. It uses a Fare (Strategy) and a Clock, both passed in. Surge wraps another Fare (Decorator). A Seat points to the hold that has it by id, not by pointer, so there's one source of truth: the flight's map of holds.
Diagrams
Class diagram
How to read it: Flight owns Seats and Holds, and creates a Booking when someone pays. It is handed two interfaces: Fare for prices and Clock for time, which is why tests can control both. ClassFare and Surge both implement Fare; the hollow diamond from Surge back to Fare is the Decorator: a Surge contains another Fare and adds to its price. A hollow diamond ◇ is a weaker has a: the Surge holds a Fare that was made outside it and can live without it.
Notation: + is public, - is private. A filled diamond ◆ means owns (the part lives and dies with its owner), a plain arrow means holds a reference to, a dashed arrow means uses, and a dashed arrow with a hollow triangle means implements this interface.
Sequence: a hold, then a late payment
How to read it: Each vertical dashed line is one object, and time runs downwards. A solid arrow is a call, a dashed arrow is the answer coming back, and the thin bars show when an object is busy. Both calls start with releaseExpired(), so the seat map is always cleaned before anyone reads it. Notice that nothing calls Flight at 10:11: the hold just stops being valid, and the next call that comes in notices.
A seat's life
How to read it: a seat moves free → held when someone holds it, and held → booked when they pay. The only way back is held → free: the time ran out, or the passenger gave up. A booked seat never goes back in this design (cancelling a booking is out of scope).
One seat, two buyers
How to read it: the lock makes "check every seat" and "mark them held" one step. Whoever takes the lock second sees the first one's change. Without the lock, both can read "free" before either writes "held", and both walk away with seat 2A.
Design principles
- All or nothing:
Holdchecks every seat (exists, not picked twice, free) before it changes any. A refused hold leaves no trace. - Time is an input:
Flightnever callstime.Now()directly. It asks theClockit was given, so tests and the demo decide what time it is. - Expire lazily: every public method first frees the holds whose time is up. No background goroutine, and no moment where a stale hold can be paid.
- One lock for check-and-change: the seats and the holds change together, so they share one lock.
- Open/closed: a new pricing rule is a new
Fare;Flightdoesn't change.
Design patterns
State (for a seat)
Three states and four transitions: free → held (hold), held → booked (pay), held → free (expiry or release). An enum plus checks in Flight is enough. Separate state classes would make sense only if each state had a lot of its own behaviour.
Strategy (for pricing)
- Why: fares change all the time: by class, by how full the flight is, by season, by promo code.
- What it solves: pricing is one swappable object,
Fare. - Without it:
if eid { … } else if lastSeats { … }grows insideHold. - Trade-off: one interface; trivial cost.
- Where:
fare.go;Holdcallsf.fare.Price(class, left).
Decorator (for the surge)
Surge is itself a Fare and holds another Fare. It asks the base for a price and adds 25% when the class is almost full. You can stack them: a student discount wrapped around a surge wrapped around class fares, and Flight still sees one Fare.
Complete solution in Go
Project structure
flights/
├── go.mod
├── seat.go Class, State, Seat
├── fare.go the Fare strategy, ClassFare and the Surge decorator
├── flight.go Flight: Hold, Pay, Release, expiry, the lock
├── flight_test.go tests, including 100 buyers for one seat
└── cmd/demo/main.go a morning before Eid
Step 1: seats and their states
// seat.go
package flights
// Class is a seat's fare class.
type Class int
const (
Economy Class = iota
Business
)
func (c Class) String() string { return [...]string{"economy", "business"}[c] }
// State is where a seat is in its life: free -> held -> booked, or held -> free again.
type State int
const (
Free State = iota
Held
Booked
)
func (s State) String() string { return [...]string{"free", "held", "booked"}[s] }
// Seat is one seat on the flight. Hold names the hold that has it while it is Held.
type Seat struct {
No string
Class Class
State State
Hold string
}
Step 2: pricing
left is how many seats of that class were free before this hold. Passing it in keeps the fare rules pure: they never look at the flight.
// fare.go
package flights
// Fare prices one seat. left is how many seats of that class were free before this hold.
type Fare interface {
Price(c Class, left int) int
}
// ClassFare is a fixed price per class.
type ClassFare map[Class]int
func (f ClassFare) Price(c Class, _ int) int { return f[c] }
// Surge wraps another Fare and adds Percent when only Below seats or fewer are left.
type Surge struct {
Base Fare
Below int
Percent int
}
func (s Surge) Price(c Class, left int) int {
p := s.Base.Price(c, left)
if left <= s.Below {
p += p * s.Percent / 100
}
return p
}
Step 3: the flight
Read Hold in two halves. The first loop only checks and adds up the price. The second loop runs only when everything passed, and changes the seats. Both halves are under the same lock.
// flight.go
package flights
import (
"fmt"
"slices"
"sync"
"time"
)
// Clock is passed in, so tests and the demo decide what time it is.
type Clock interface{ Now() time.Time }
// Hold keeps seats for one passenger until Expires. Nobody else can take them meanwhile.
type Hold struct {
ID string
Passenger string
Seats []string
Total int
Expires time.Time
}
// Booking is a paid, confirmed reservation.
type Booking struct {
PNR string
Passenger string
Seats []string
Total int
}
// Flight owns its seats and every hold on them. One lock guards all of it.
type Flight struct {
mu sync.Mutex
Code string
order []string // seat numbers in map order: 1A, 1B, 2A ...
seats map[string]*Seat
holds map[string]*Hold
expired map[string]time.Time // hold id -> when it ran out, for a clear error
clock Clock
fare Fare
holdFor time.Duration
nextHold int
nextPNR int
}
func NewFlight(code string, clock Clock, fare Fare, holdFor time.Duration) *Flight {
return &Flight{Code: code, seats: map[string]*Seat{}, holds: map[string]*Hold{},
expired: map[string]time.Time{}, clock: clock, fare: fare, holdFor: holdFor}
}
// AddRow adds one row of seats, e.g. AddRow(2, Economy, "ABC") adds 2A 2B 2C.
func (f *Flight) AddRow(row int, class Class, letters string) {
f.mu.Lock()
defer f.mu.Unlock()
for _, l := range letters {
no := fmt.Sprintf("%d%c", row, l)
f.seats[no] = &Seat{No: no, Class: class}
f.order = append(f.order, no)
}
}
// Hold keeps all the seats for one passenger, or none of them.
func (f *Flight) Hold(passenger string, seatNos ...string) (Hold, error) {
f.mu.Lock()
defer f.mu.Unlock()
f.releaseExpired()
if len(seatNos) == 0 {
return Hold{}, fmt.Errorf("pick at least one seat")
}
left := map[Class]int{} // free seats per class, before this hold
for _, s := range f.seats {
if s.State == Free {
left[s.Class]++
}
}
// First check every seat, without touching anything.
total, seen := 0, map[string]bool{}
for _, no := range seatNos {
s, ok := f.seats[no]
switch {
case !ok:
return Hold{}, fmt.Errorf("no seat %s", no)
case seen[no]:
return Hold{}, fmt.Errorf("seat %s picked twice", no)
case s.State != Free:
return Hold{}, fmt.Errorf("seat %s is %s", no, s.State)
}
seen[no] = true
total += f.fare.Price(s.Class, left[s.Class])
}
// All free: now take them.
f.nextHold++
h := &Hold{ID: fmt.Sprintf("H%d", f.nextHold), Passenger: passenger,
Seats: slices.Clone(seatNos), Total: total, Expires: f.clock.Now().Add(f.holdFor)}
for _, no := range seatNos {
f.seats[no].State, f.seats[no].Hold = Held, h.ID
}
f.holds[h.ID] = h
return *h, nil
}
// Pay turns a live hold into a booking. A hold that ran out is refused.
func (f *Flight) Pay(holdID string) (Booking, error) {
f.mu.Lock()
defer f.mu.Unlock()
f.releaseExpired()
h, ok := f.holds[holdID]
if !ok {
if at, gone := f.expired[holdID]; gone {
return Booking{}, fmt.Errorf("hold %s expired at %s", holdID, at.Format("15:04"))
}
return Booking{}, fmt.Errorf("no hold %s", holdID)
}
for _, no := range h.Seats {
f.seats[no].State, f.seats[no].Hold = Booked, ""
}
delete(f.holds, holdID)
f.nextPNR++
return Booking{PNR: fmt.Sprintf("PNR-%d", f.nextPNR), Passenger: h.Passenger,
Seats: slices.Clone(h.Seats), Total: h.Total}, nil
}
// Release gives the seats back at once: the passenger closed the app or changed their mind.
func (f *Flight) Release(holdID string) error {
f.mu.Lock()
defer f.mu.Unlock()
h, ok := f.holds[holdID]
if !ok {
return fmt.Errorf("no hold %s", holdID)
}
f.free(h)
delete(f.holds, holdID)
return nil
}
// Seats returns a copy of every seat, in map order.
func (f *Flight) Seats() []Seat {
f.mu.Lock()
defer f.mu.Unlock()
f.releaseExpired()
out := make([]Seat, 0, len(f.order))
for _, no := range f.order {
out = append(out, *f.seats[no])
}
return out
}
// releaseExpired frees the seats of every hold whose time is up. Callers hold the lock.
func (f *Flight) releaseExpired() {
now := f.clock.Now()
for id, h := range f.holds {
if !now.Before(h.Expires) {
f.free(h)
delete(f.holds, id)
f.expired[id] = h.Expires
}
}
}
func (f *Flight) free(h *Hold) {
for _, no := range h.Seats {
f.seats[no].State, f.seats[no].Hold = Free, ""
}
}
Step 4: run it
// cmd/demo/main.go
package main
import (
"fmt"
"strings"
"time"
"flights"
)
// fakeClock lets the story jump from minute to minute.
type fakeClock struct{ t time.Time }
func (c *fakeClock) Now() time.Time { return c.t }
func (c *fakeClock) set(hhmm string) { c.t, _ = time.Parse("15:04", hhmm) }
func main() {
clock := &fakeClock{}
fare := flights.Surge{
Base: flights.ClassFare{flights.Economy: 5000, flights.Business: 12000},
Below: 1, Percent: 25,
}
f := flights.NewFlight("CF-141", clock, fare, 10*time.Minute)
f.AddRow(1, flights.Business, "AB")
f.AddRow(2, flights.Economy, "ABC")
f.AddRow(3, flights.Economy, "ABC")
fmt.Println("flight CF-141 Dhaka -> Cox's Bazar: 2 business, 6 economy, holds last 10 min")
hold := func(at, who string, seats ...string) {
clock.set(at)
label := fmt.Sprintf("%s %-6s holds %s", at, who, strings.Join(seats, " "))
h, err := f.Hold(who, seats...)
if err != nil {
fmt.Printf("%-28s -> refused: %v\n", label, err)
return
}
fmt.Printf("%-28s -> %s, %d tk, pay by %s\n", label, h.ID, h.Total, h.Expires.Format("15:04"))
}
pay := func(at, who, id string) {
clock.set(at)
label := fmt.Sprintf("%s %-6s pays %s", at, who, id)
b, err := f.Pay(id)
if err != nil {
fmt.Printf("%-28s -> refused: %v\n", label, err)
return
}
fmt.Printf("%-28s -> booked %s: %s\n", label, b.PNR, strings.Join(b.Seats, " "))
}
hold("10:00", "Rahim", "2A", "2B")
hold("10:01", "Nadia", "2B", "2C")
hold("10:01", "Nadia", "2C", "3A")
pay("10:03", "Rahim", "H1")
hold("10:05", "Tania", "1A")
pay("10:06", "Tania", "H3")
pay("10:12", "Nadia", "H2")
hold("10:12", "Sabbir", "2C", "3A", "3B")
hold("10:13", "Karim", "3C")
clock.set("10:14")
f.Release("H5")
fmt.Printf("%-28s -> 3C is free again\n", "10:14 Karim gives up H5")
by := map[flights.State][]string{}
for _, s := range f.Seats() {
by[s.State] = append(by[s.State], s.No)
}
fmt.Printf("10:14 booked: %s | held: %s | free: %s\n",
strings.Join(by[flights.Booked], " "), strings.Join(by[flights.Held], " "), strings.Join(by[flights.Free], " "))
}
$ go run ./cmd/demo
flight CF-141 Dhaka -> Cox's Bazar: 2 business, 6 economy, holds last 10 min
10:00 Rahim holds 2A 2B -> H1, 10000 tk, pay by 10:10
10:01 Nadia holds 2B 2C -> refused: seat 2B is held
10:01 Nadia holds 2C 3A -> H2, 10000 tk, pay by 10:11
10:03 Rahim pays H1 -> booked PNR-1: 2A 2B
10:05 Tania holds 1A -> H3, 12000 tk, pay by 10:15
10:06 Tania pays H3 -> booked PNR-2: 1A
10:12 Nadia pays H2 -> refused: hold H2 expired at 10:11
10:12 Sabbir holds 2C 3A 3B -> H4, 15000 tk, pay by 10:22
10:13 Karim holds 3C -> H5, 6250 tk, pay by 10:23
10:14 Karim gives up H5 -> 3C is free again
10:14 booked: 1A 2A 2B | held: 2C 3A 3B | free: 1B 3C
Reading the output:
- 10:00: Rahim holds 2A and 2B: two economy seats at 5000 each, deadline 10:10.
- 10:01: Nadia asks for 2B and 2C. 2B is Rahim's, so the whole hold is refused, and 2C stays free (that's the all-or-nothing rule). She then holds 2C and 3A.
- 10:03: Rahim pays in time and gets PNR-1. Hold ids and PNRs are separate counters, which is why Tania's hold is H3 but her booking is PNR-2.
- 10:12: Nadia tries to pay, but her deadline was 10:11. Her seats were freed the moment someone touched the flight after 10:11, so the payment is refused.
- 10:12: Sabbir takes 2C, 3A and 3B: the seats Nadia lost, plus one more. 4 economy seats were free, so no surge: 15000.
- 10:13: Karim holds 3C, the last economy seat: 5000 + 25% = 6250.
- 10:14: Karim gives up, and 3C is free again at once. The final line is the seat map.
The other five languages
from __future__ import annotations
import threading
from dataclasses import dataclass, field
from enum import Enum
from typing import Protocol
# Time is minutes since midnight, to keep the demo short. 10:05 is 605.
def hhmm(m: int) -> str:
return f"{m // 60:02d}:{m % 60:02d}"
class Class(Enum):
ECONOMY = "economy"
BUSINESS = "business"
class State(Enum):
"""free -> held -> booked, or held -> free again."""
FREE = "free"
HELD = "held"
BOOKED = "booked"
@dataclass
class Seat:
no: str
cls: Class
state: State = State.FREE
hold: str = ""
class Clock(Protocol):
def now(self) -> int: ...
class Fare(Protocol):
"""Prices one seat. left = free seats of that class before this hold."""
def price(self, cls: Class, left: int) -> int: ...
class ClassFare:
def __init__(self, prices: dict[Class, int]):
self.prices = prices
def price(self, cls: Class, left: int) -> int:
return self.prices[cls]
class Surge:
"""Wraps another Fare and adds percent when only `below` seats or fewer are left."""
def __init__(self, base: Fare, below: int, percent: int):
self.base, self.below, self.percent = base, below, percent
def price(self, cls: Class, left: int) -> int:
p = self.base.price(cls, left)
return p + p * self.percent // 100 if left <= self.below else p
@dataclass
class Hold:
id: str
passenger: str
seats: list[str]
total: int
expires: int
@dataclass
class Booking:
pnr: str
passenger: str
seats: list[str]
total: int
class Refused(Exception):
pass
@dataclass
class Flight:
code: str
clock: Clock
fare: Fare
hold_for: int
seats: dict[str, Seat] = field(default_factory=dict) # insertion order = map order
holds: dict[str, Hold] = field(default_factory=dict)
expired: dict[str, int] = field(default_factory=dict)
next_hold: int = 0
next_pnr: int = 0
lock: threading.Lock = field(default_factory=threading.Lock)
def add_row(self, row: int, cls: Class, letters: str) -> None:
with self.lock:
for l in letters:
self.seats[f"{row}{l}"] = Seat(f"{row}{l}", cls)
def hold(self, passenger: str, *nos: str) -> Hold:
"""Keeps all the seats for one passenger, or none of them."""
with self.lock:
self._release_expired()
if not nos:
raise Refused("pick at least one seat")
left: dict[Class, int] = {}
for s in self.seats.values():
if s.state is State.FREE:
left[s.cls] = left.get(s.cls, 0) + 1
total, seen = 0, set()
for no in nos: # check every seat first, touch nothing
s = self.seats.get(no)
if s is None:
raise Refused(f"no seat {no}")
if no in seen:
raise Refused(f"seat {no} picked twice")
if s.state is not State.FREE:
raise Refused(f"seat {no} is {s.state.value}")
seen.add(no)
total += self.fare.price(s.cls, left.get(s.cls, 0))
self.next_hold += 1
h = Hold(f"H{self.next_hold}", passenger, list(nos), total, self.clock.now() + self.hold_for)
for no in nos:
self.seats[no].state, self.seats[no].hold = State.HELD, h.id
self.holds[h.id] = h
return h
def pay(self, hold_id: str) -> Booking:
with self.lock:
self._release_expired()
h = self.holds.pop(hold_id, None)
if h is None:
if hold_id in self.expired:
raise Refused(f"hold {hold_id} expired at {hhmm(self.expired[hold_id])}")
raise Refused(f"no hold {hold_id}")
for no in h.seats:
self.seats[no].state, self.seats[no].hold = State.BOOKED, ""
self.next_pnr += 1
return Booking(f"PNR-{self.next_pnr}", h.passenger, list(h.seats), h.total)
def release(self, hold_id: str) -> None:
with self.lock:
h = self.holds.pop(hold_id, None)
if h is None:
raise Refused(f"no hold {hold_id}")
self._free(h)
def all_seats(self) -> list[Seat]:
with self.lock:
self._release_expired()
return [Seat(s.no, s.cls, s.state, s.hold) for s in self.seats.values()]
def _release_expired(self) -> None:
now = self.clock.now()
for hid in [i for i, h in self.holds.items() if now >= h.expires]:
h = self.holds.pop(hid)
self._free(h)
self.expired[hid] = h.expires
def _free(self, h: Hold) -> None:
for no in h.seats:
self.seats[no].state, self.seats[no].hold = State.FREE, ""
class FakeClock:
def __init__(self):
self.t = 0
def now(self) -> int:
return self.t
def set(self, s: str) -> None:
h, m = s.split(":")
self.t = int(h) * 60 + int(m)
def main() -> None:
clock = FakeClock()
fare = Surge(ClassFare({Class.ECONOMY: 5000, Class.BUSINESS: 12000}), below=1, percent=25)
f = Flight("CF-141", clock, fare, hold_for=10)
f.add_row(1, Class.BUSINESS, "AB")
f.add_row(2, Class.ECONOMY, "ABC")
f.add_row(3, Class.ECONOMY, "ABC")
print("flight CF-141 Dhaka -> Cox's Bazar: 2 business, 6 economy, holds last 10 min")
def hold(at: str, who: str, *seats: str) -> None:
clock.set(at)
label = f"{at} {who:<6} holds {' '.join(seats)}"
try:
h = f.hold(who, *seats)
print(f"{label:<28} -> {h.id}, {h.total} tk, pay by {hhmm(h.expires)}")
except Refused as e:
print(f"{label:<28} -> refused: {e}")
def pay(at: str, who: str, hid: str) -> None:
clock.set(at)
label = f"{at} {who:<6} pays {hid}"
try:
b = f.pay(hid)
print(f"{label:<28} -> booked {b.pnr}: {' '.join(b.seats)}")
except Refused as e:
print(f"{label:<28} -> refused: {e}")
hold("10:00", "Rahim", "2A", "2B")
hold("10:01", "Nadia", "2B", "2C")
hold("10:01", "Nadia", "2C", "3A")
pay("10:03", "Rahim", "H1")
hold("10:05", "Tania", "1A")
pay("10:06", "Tania", "H3")
pay("10:12", "Nadia", "H2")
hold("10:12", "Sabbir", "2C", "3A", "3B")
hold("10:13", "Karim", "3C")
clock.set("10:14")
f.release("H5")
print(f"{'10:14 Karim gives up H5':<28} -> 3C is free again")
by: dict[State, list[str]] = {s: [] for s in State}
for s in f.all_seats():
by[s.state].append(s.no)
print(f"10:14 booked: {' '.join(by[State.BOOKED])} | held: {' '.join(by[State.HELD])} | free: {' '.join(by[State.FREE])}")
if __name__ == "__main__":
main()// Time is minutes since midnight, to keep the demo short. 10:05 is 605.
const hhmm = (m: number) => `${String(Math.floor(m / 60)).padStart(2, "0")}:${String(m % 60).padStart(2, "0")}`;
type Class = "economy" | "business";
type State = "free" | "held" | "booked"; // free -> held -> booked, or held -> free again
interface Seat { no: string; cls: Class; state: State; hold: string }
interface Clock { now(): number }
/** Prices one seat. left = free seats of that class before this hold. */
interface Fare { price(cls: Class, left: number): number }
class ClassFare implements Fare {
prices: Record<Class, number>;
constructor(prices: Record<Class, number>) { this.prices = prices; }
price(cls: Class): number { return this.prices[cls]; }
}
/** Wraps another Fare and adds percent when only `below` seats or fewer are left. */
class Surge implements Fare {
base: Fare; below: number; percent: number;
constructor(base: Fare, below: number, percent: number) {
this.base = base; this.below = below; this.percent = percent;
}
price(cls: Class, left: number): number {
const p = this.base.price(cls, left);
return left <= this.below ? p + Math.floor((p * this.percent) / 100) : p;
}
}
interface Hold { id: string; passenger: string; seats: string[]; total: number; expires: number }
interface Booking { pnr: string; passenger: string; seats: string[]; total: number }
class Refused extends Error {}
// One thread runs JavaScript, so a method can't be interrupted halfway: no lock needed.
class Flight {
seats = new Map<string, Seat>(); // insertion order = map order
holds = new Map<string, Hold>();
expired = new Map<string, number>();
nextHold = 0;
nextPnr = 0;
code: string; clock: Clock; fare: Fare; holdFor: number;
constructor(code: string, clock: Clock, fare: Fare, holdFor: number) {
this.code = code; this.clock = clock; this.fare = fare; this.holdFor = holdFor;
}
addRow(row: number, cls: Class, letters: string): void {
for (const l of letters) this.seats.set(`${row}${l}`, { no: `${row}${l}`, cls, state: "free", hold: "" });
}
/** Keeps all the seats for one passenger, or none of them. */
hold(passenger: string, ...nos: string[]): Hold {
this.releaseExpired();
if (nos.length === 0) throw new Refused("pick at least one seat");
const left = new Map<Class, number>();
for (const s of this.seats.values()) if (s.state === "free") left.set(s.cls, (left.get(s.cls) ?? 0) + 1);
let total = 0;
const seen = new Set<string>();
for (const no of nos) { // check every seat first, touch nothing
const s = this.seats.get(no);
if (!s) throw new Refused(`no seat ${no}`);
if (seen.has(no)) throw new Refused(`seat ${no} picked twice`);
if (s.state !== "free") throw new Refused(`seat ${no} is ${s.state}`);
seen.add(no);
total += this.fare.price(s.cls, left.get(s.cls) ?? 0);
}
const h: Hold = { id: `H${++this.nextHold}`, passenger, seats: [...nos], total, expires: this.clock.now() + this.holdFor };
for (const no of nos) Object.assign(this.seats.get(no)!, { state: "held", hold: h.id });
this.holds.set(h.id, h);
return h;
}
pay(holdId: string): Booking {
this.releaseExpired();
const h = this.holds.get(holdId);
if (!h) {
const at = this.expired.get(holdId);
throw new Refused(at !== undefined ? `hold ${holdId} expired at ${hhmm(at)}` : `no hold ${holdId}`);
}
for (const no of h.seats) Object.assign(this.seats.get(no)!, { state: "booked", hold: "" });
this.holds.delete(holdId);
return { pnr: `PNR-${++this.nextPnr}`, passenger: h.passenger, seats: [...h.seats], total: h.total };
}
release(holdId: string): void {
const h = this.holds.get(holdId);
if (!h) throw new Refused(`no hold ${holdId}`);
this.free(h);
this.holds.delete(holdId);
}
allSeats(): Seat[] {
this.releaseExpired();
return [...this.seats.values()].map((s) => ({ ...s }));
}
private releaseExpired(): void {
const now = this.clock.now();
for (const [id, h] of this.holds) {
if (now >= h.expires) {
this.free(h);
this.holds.delete(id);
this.expired.set(id, h.expires);
}
}
}
private free(h: Hold): void {
for (const no of h.seats) Object.assign(this.seats.get(no)!, { state: "free", hold: "" });
}
}
class FakeClock implements Clock {
t = 0;
now(): number { return this.t; }
set(s: string): void { const [h, m] = s.split(":").map(Number); this.t = h * 60 + m; }
}
function main(): void {
const clock = new FakeClock();
const fare = new Surge(new ClassFare({ economy: 5000, business: 12000 }), 1, 25);
const f = new Flight("CF-141", clock, fare, 10);
f.addRow(1, "business", "AB");
f.addRow(2, "economy", "ABC");
f.addRow(3, "economy", "ABC");
console.log("flight CF-141 Dhaka -> Cox's Bazar: 2 business, 6 economy, holds last 10 min");
const attempt = (label: string, fn: () => string) => {
try {
console.log(`${label.padEnd(28)} -> ${fn()}`);
} catch (e) {
if (!(e instanceof Refused)) throw e;
console.log(`${label.padEnd(28)} -> refused: ${e.message}`);
}
};
const hold = (at: string, who: string, ...seats: string[]) => {
clock.set(at);
attempt(`${at} ${who.padEnd(6)} holds ${seats.join(" ")}`, () => {
const h = f.hold(who, ...seats);
return `${h.id}, ${h.total} tk, pay by ${hhmm(h.expires)}`;
});
};
const pay = (at: string, who: string, id: string) => {
clock.set(at);
attempt(`${at} ${who.padEnd(6)} pays ${id}`, () => {
const b = f.pay(id);
return `booked ${b.pnr}: ${b.seats.join(" ")}`;
});
};
hold("10:00", "Rahim", "2A", "2B");
hold("10:01", "Nadia", "2B", "2C");
hold("10:01", "Nadia", "2C", "3A");
pay("10:03", "Rahim", "H1");
hold("10:05", "Tania", "1A");
pay("10:06", "Tania", "H3");
pay("10:12", "Nadia", "H2");
hold("10:12", "Sabbir", "2C", "3A", "3B");
hold("10:13", "Karim", "3C");
clock.set("10:14");
f.release("H5");
console.log(`${"10:14 Karim gives up H5".padEnd(28)} -> 3C is free again`);
const by: Record<State, string[]> = { free: [], held: [], booked: [] };
for (const s of f.allSeats()) by[s.state].push(s.no);
console.log(`10:14 booked: ${by.booked.join(" ")} | held: ${by.held.join(" ")} | free: ${by.free.join(" ")}`);
}
main();use std::collections::{HashMap, HashSet};
use std::sync::Mutex;
// Time is minutes since midnight, to keep the demo short. 10:05 is 605.
fn hhmm(m: u32) -> String {
format!("{:02}:{:02}", m / 60, m % 60)
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
enum Class {
Economy,
Business,
}
// free -> held -> booked, or held -> free again
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum State {
Free,
Held,
Booked,
}
impl State {
fn name(self) -> &'static str {
match self {
State::Free => "free",
State::Held => "held",
State::Booked => "booked",
}
}
}
#[derive(Clone, Debug)]
struct Seat {
no: String,
class: Class,
state: State,
hold: String,
}
trait Clock: Send + Sync {
fn now(&self) -> u32;
}
/// Prices one seat. `left` = free seats of that class before this hold.
trait Fare: Send + Sync {
fn price(&self, class: Class, left: u32) -> u32;
}
struct ClassFare(HashMap<Class, u32>);
impl Fare for ClassFare {
fn price(&self, class: Class, _left: u32) -> u32 {
self.0[&class]
}
}
/// Wraps another Fare and adds `percent` when only `below` seats or fewer are left.
struct Surge {
base: Box<dyn Fare>,
below: u32,
percent: u32,
}
impl Fare for Surge {
fn price(&self, class: Class, left: u32) -> u32 {
let p = self.base.price(class, left);
if left <= self.below { p + p * self.percent / 100 } else { p }
}
}
#[derive(Clone)]
struct Hold {
id: String,
passenger: String,
seats: Vec<String>,
total: u32,
expires: u32,
}
struct Booking {
pnr: String,
#[allow(dead_code)]
passenger: String,
seats: Vec<String>,
#[allow(dead_code)]
total: u32,
}
// Everything that changes lives inside the Mutex, so you can't touch it without the lock.
#[derive(Default)]
struct Inner {
order: Vec<String>,
seats: HashMap<String, Seat>,
holds: HashMap<String, Hold>,
expired: HashMap<String, u32>,
next_hold: u32,
next_pnr: u32,
}
impl Inner {
fn release_expired(&mut self, now: u32) {
let gone: Vec<String> = self.holds.values().filter(|h| now >= h.expires).map(|h| h.id.clone()).collect();
for id in gone {
let h = self.holds.remove(&id).unwrap();
self.free(&h);
self.expired.insert(id, h.expires);
}
}
fn free(&mut self, h: &Hold) {
for no in &h.seats {
let s = self.seats.get_mut(no).unwrap();
s.state = State::Free;
s.hold.clear();
}
}
}
struct Flight {
#[allow(dead_code)]
code: String,
clock: Box<dyn Clock>,
fare: Box<dyn Fare>,
hold_for: u32,
inner: Mutex<Inner>,
}
impl Flight {
fn new(code: &str, clock: Box<dyn Clock>, fare: Box<dyn Fare>, hold_for: u32) -> Self {
Flight { code: code.into(), clock, fare, hold_for, inner: Mutex::new(Inner::default()) }
}
fn add_row(&self, row: u32, class: Class, letters: &str) {
let mut g = self.inner.lock().unwrap();
for l in letters.chars() {
let no = format!("{row}{l}");
g.seats.insert(no.clone(), Seat { no: no.clone(), class, state: State::Free, hold: String::new() });
g.order.push(no);
}
}
/// Keeps all the seats for one passenger, or none of them.
fn hold(&self, passenger: &str, nos: &[&str]) -> Result<Hold, String> {
let now = self.clock.now();
let mut g = self.inner.lock().unwrap();
g.release_expired(now);
if nos.is_empty() {
return Err("pick at least one seat".into());
}
let mut left: HashMap<Class, u32> = HashMap::new();
for s in g.seats.values().filter(|s| s.state == State::Free) {
*left.entry(s.class).or_default() += 1;
}
let (mut total, mut seen) = (0, HashSet::new());
for &no in nos {
// check every seat first, touch nothing
let s = g.seats.get(no).ok_or(format!("no seat {no}"))?;
if !seen.insert(no) {
return Err(format!("seat {no} picked twice"));
}
if s.state != State::Free {
return Err(format!("seat {no} is {}", s.state.name()));
}
total += self.fare.price(s.class, left.get(&s.class).copied().unwrap_or(0));
}
g.next_hold += 1;
let h = Hold {
id: format!("H{}", g.next_hold),
passenger: passenger.into(),
seats: nos.iter().map(|s| s.to_string()).collect(),
total,
expires: now + self.hold_for,
};
for &no in nos {
let s = g.seats.get_mut(no).unwrap();
s.state = State::Held;
s.hold = h.id.clone();
}
g.holds.insert(h.id.clone(), h.clone());
Ok(h)
}
fn pay(&self, hold_id: &str) -> Result<Booking, String> {
let now = self.clock.now();
let mut g = self.inner.lock().unwrap();
g.release_expired(now);
let Some(h) = g.holds.remove(hold_id) else {
return Err(match g.expired.get(hold_id) {
Some(&at) => format!("hold {hold_id} expired at {}", hhmm(at)),
None => format!("no hold {hold_id}"),
});
};
for no in &h.seats {
let s = g.seats.get_mut(no).unwrap();
s.state = State::Booked;
s.hold.clear();
}
g.next_pnr += 1;
Ok(Booking { pnr: format!("PNR-{}", g.next_pnr), passenger: h.passenger, seats: h.seats, total: h.total })
}
fn release(&self, hold_id: &str) -> Result<(), String> {
let mut g = self.inner.lock().unwrap();
let h = g.holds.remove(hold_id).ok_or(format!("no hold {hold_id}"))?;
g.free(&h);
Ok(())
}
fn all_seats(&self) -> Vec<Seat> {
let now = self.clock.now();
let mut g = self.inner.lock().unwrap();
g.release_expired(now);
g.order.iter().map(|no| g.seats[no].clone()).collect()
}
}
// The demo's clock: the story sets the time from outside, through a shared cell.
struct FakeClock(std::sync::Arc<Mutex<u32>>);
impl Clock for FakeClock {
fn now(&self) -> u32 {
*self.0.lock().unwrap()
}
}
fn main() {
let t = std::sync::Arc::new(Mutex::new(0u32));
let set = |s: &str| {
let (h, m) = s.split_once(':').unwrap();
*t.lock().unwrap() = h.parse::<u32>().unwrap() * 60 + m.parse::<u32>().unwrap();
};
let base = ClassFare(HashMap::from([(Class::Economy, 5000), (Class::Business, 12000)]));
let fare = Surge { base: Box::new(base), below: 1, percent: 25 };
let f = Flight::new("CF-141", Box::new(FakeClock(t.clone())), Box::new(fare), 10);
f.add_row(1, Class::Business, "AB");
f.add_row(2, Class::Economy, "ABC");
f.add_row(3, Class::Economy, "ABC");
println!("flight CF-141 Dhaka -> Cox's Bazar: 2 business, 6 economy, holds last 10 min");
let hold = |at: &str, who: &str, seats: &[&str]| {
set(at);
let label = format!("{at} {who:<6} holds {}", seats.join(" "));
match f.hold(who, seats) {
Ok(h) => println!("{label:<28} -> {}, {} tk, pay by {}", h.id, h.total, hhmm(h.expires)),
Err(e) => println!("{label:<28} -> refused: {e}"),
}
};
let pay = |at: &str, who: &str, id: &str| {
set(at);
let label = format!("{at} {who:<6} pays {id}");
match f.pay(id) {
Ok(b) => println!("{label:<28} -> booked {}: {}", b.pnr, b.seats.join(" ")),
Err(e) => println!("{label:<28} -> refused: {e}"),
}
};
hold("10:00", "Rahim", &["2A", "2B"]);
hold("10:01", "Nadia", &["2B", "2C"]);
hold("10:01", "Nadia", &["2C", "3A"]);
pay("10:03", "Rahim", "H1");
hold("10:05", "Tania", &["1A"]);
pay("10:06", "Tania", "H3");
pay("10:12", "Nadia", "H2");
hold("10:12", "Sabbir", &["2C", "3A", "3B"]);
hold("10:13", "Karim", &["3C"]);
set("10:14");
f.release("H5").unwrap();
println!("{:<28} -> 3C is free again", "10:14 Karim gives up H5");
let seats = f.all_seats();
let by = |st: State| seats.iter().filter(|s| s.state == st).map(|s| s.no.as_str()).collect::<Vec<_>>().join(" ");
println!("10:14 booked: {} | held: {} | free: {}", by(State::Booked), by(State::Held), by(State::Free));
}import java.util.*;
public class Main {
// Time is minutes since midnight, to keep the demo short. 10:05 is 605.
static String hhmm(int m) { return String.format("%02d:%02d", m / 60, m % 60); }
enum SeatClass { ECONOMY, BUSINESS }
// free -> held -> booked, or held -> free again
enum State {
FREE, HELD, BOOKED;
String label() { return name().toLowerCase(); }
}
static final class Seat {
final String no;
final SeatClass cls;
State state = State.FREE;
String hold = "";
Seat(String no, SeatClass cls) { this.no = no; this.cls = cls; }
}
interface Clock { int now(); }
/** Prices one seat. left = free seats of that class before this hold. */
interface Fare { int price(SeatClass cls, int left); }
record ClassFare(Map<SeatClass, Integer> prices) implements Fare {
public int price(SeatClass cls, int left) { return prices.get(cls); }
}
/** Wraps another Fare and adds percent when only `below` seats or fewer are left. */
record Surge(Fare base, int below, int percent) implements Fare {
public int price(SeatClass cls, int left) {
int p = base.price(cls, left);
return left <= below ? p + p * percent / 100 : p;
}
}
record Hold(String id, String passenger, List<String> seats, int total, int expires) {}
record Booking(String pnr, String passenger, List<String> seats, int total) {}
static final class Refused extends Exception {
private static final long serialVersionUID = 1L;
Refused(String msg) { super(msg); }
}
static final class Flight {
final String code;
private final Clock clock;
private final Fare fare;
private final int holdFor;
private final Map<String, Seat> seats = new LinkedHashMap<>(); // keeps map order
private final Map<String, Hold> holds = new HashMap<>();
private final Map<String, Integer> expired = new HashMap<>();
private int nextHold, nextPnr;
Flight(String code, Clock clock, Fare fare, int holdFor) {
this.code = code; this.clock = clock; this.fare = fare; this.holdFor = holdFor;
}
synchronized void addRow(int row, SeatClass cls, String letters) {
for (char l : letters.toCharArray()) seats.put(row + "" + l, new Seat(row + "" + l, cls));
}
/** Keeps all the seats for one passenger, or none of them. */
synchronized Hold hold(String passenger, String... nos) throws Refused {
releaseExpired();
if (nos.length == 0) throw new Refused("pick at least one seat");
Map<SeatClass, Integer> left = new EnumMap<>(SeatClass.class);
for (Seat s : seats.values()) if (s.state == State.FREE) left.merge(s.cls, 1, Integer::sum);
int total = 0;
Set<String> seen = new HashSet<>();
for (String no : nos) { // check every seat first, touch nothing
Seat s = seats.get(no);
if (s == null) throw new Refused("no seat " + no);
if (!seen.add(no)) throw new Refused("seat " + no + " picked twice");
if (s.state != State.FREE) throw new Refused("seat " + no + " is " + s.state.label());
total += fare.price(s.cls, left.getOrDefault(s.cls, 0));
}
Hold h = new Hold("H" + ++nextHold, passenger, List.of(nos), total, clock.now() + holdFor);
for (String no : nos) { seats.get(no).state = State.HELD; seats.get(no).hold = h.id(); }
holds.put(h.id(), h);
return h;
}
synchronized Booking pay(String holdId) throws Refused {
releaseExpired();
Hold h = holds.remove(holdId);
if (h == null) {
Integer at = expired.get(holdId);
throw new Refused(at != null ? "hold " + holdId + " expired at " + hhmm(at) : "no hold " + holdId);
}
for (String no : h.seats()) { seats.get(no).state = State.BOOKED; seats.get(no).hold = ""; }
return new Booking("PNR-" + ++nextPnr, h.passenger(), h.seats(), h.total());
}
synchronized void release(String holdId) throws Refused {
Hold h = holds.remove(holdId);
if (h == null) throw new Refused("no hold " + holdId);
free(h);
}
synchronized Map<State, List<String>> seatsByState() {
releaseExpired();
Map<State, List<String>> by = new EnumMap<>(State.class);
for (State st : State.values()) by.put(st, new ArrayList<>());
for (Seat s : seats.values()) by.get(s.state).add(s.no);
return by;
}
private void releaseExpired() {
int now = clock.now();
Iterator<Hold> it = holds.values().iterator();
while (it.hasNext()) {
Hold h = it.next();
if (now >= h.expires()) { free(h); it.remove(); expired.put(h.id(), h.expires()); }
}
}
private void free(Hold h) {
for (String no : h.seats()) { seats.get(no).state = State.FREE; seats.get(no).hold = ""; }
}
}
static int t = 0;
static void set(String s) {
String[] p = s.split(":");
t = Integer.parseInt(p[0]) * 60 + Integer.parseInt(p[1]);
}
static Flight f;
static void hold(String at, String who, String... seats) {
set(at);
String label = String.format("%s %-6s holds %s", at, who, String.join(" ", seats));
try {
Hold h = f.hold(who, seats);
System.out.printf("%-28s -> %s, %d tk, pay by %s%n", label, h.id(), h.total(), hhmm(h.expires()));
} catch (Refused e) {
System.out.printf("%-28s -> refused: %s%n", label, e.getMessage());
}
}
static void pay(String at, String who, String id) {
set(at);
String label = String.format("%s %-6s pays %s", at, who, id);
try {
Booking b = f.pay(id);
System.out.printf("%-28s -> booked %s: %s%n", label, b.pnr(), String.join(" ", b.seats()));
} catch (Refused e) {
System.out.printf("%-28s -> refused: %s%n", label, e.getMessage());
}
}
public static void main(String[] args) throws Refused {
Fare fare = new Surge(new ClassFare(Map.of(SeatClass.ECONOMY, 5000, SeatClass.BUSINESS, 12000)), 1, 25);
f = new Flight("CF-141", () -> t, fare, 10);
f.addRow(1, SeatClass.BUSINESS, "AB");
f.addRow(2, SeatClass.ECONOMY, "ABC");
f.addRow(3, SeatClass.ECONOMY, "ABC");
System.out.println("flight CF-141 Dhaka -> Cox's Bazar: 2 business, 6 economy, holds last 10 min");
hold("10:00", "Rahim", "2A", "2B");
hold("10:01", "Nadia", "2B", "2C");
hold("10:01", "Nadia", "2C", "3A");
pay("10:03", "Rahim", "H1");
hold("10:05", "Tania", "1A");
pay("10:06", "Tania", "H3");
pay("10:12", "Nadia", "H2");
hold("10:12", "Sabbir", "2C", "3A", "3B");
hold("10:13", "Karim", "3C");
set("10:14");
f.release("H5");
System.out.printf("%-28s -> 3C is free again%n", "10:14 Karim gives up H5");
var by = f.seatsByState();
System.out.printf("10:14 booked: %s | held: %s | free: %s%n", String.join(" ", by.get(State.BOOKED)),
String.join(" ", by.get(State.HELD)), String.join(" ", by.get(State.FREE)));
}
}#include <cstdio>
#include <map>
#include <memory>
#include <mutex>
#include <optional>
#include <set>
#include <stdexcept>
#include <string>
#include <unordered_map>
#include <vector>
// Time is minutes since midnight, to keep the demo short. 10:05 is 605.
std::string hhmm(int m) {
char buf[16];
std::snprintf(buf, sizeof buf, "%02d:%02d", m / 60, m % 60);
return buf;
}
std::string join(const std::vector<std::string>& v) {
std::string out;
for (size_t i = 0; i < v.size(); i++) out += (i ? " " : "") + v[i];
return out;
}
enum class SeatClass { Economy, Business };
enum class State { Free, Held, Booked }; // free -> held -> booked, or held -> free again
const char* name(State s) { return s == State::Free ? "free" : s == State::Held ? "held" : "booked"; }
struct Seat {
std::string no;
SeatClass cls;
State state = State::Free;
std::string hold;
};
struct Clock {
virtual ~Clock() = default;
virtual int now() const = 0;
};
// Prices one seat. left = free seats of that class before this hold.
struct Fare {
virtual ~Fare() = default;
virtual int price(SeatClass cls, int left) const = 0;
};
struct ClassFare : Fare {
std::map<SeatClass, int> prices;
explicit ClassFare(std::map<SeatClass, int> p) : prices(std::move(p)) {}
int price(SeatClass cls, int) const override { return prices.at(cls); }
};
// Wraps another Fare and adds percent when only `below` seats or fewer are left.
struct Surge : Fare {
std::unique_ptr<Fare> base;
int below, percent;
Surge(std::unique_ptr<Fare> b, int bl, int pc) : base(std::move(b)), below(bl), percent(pc) {}
int price(SeatClass cls, int left) const override {
int p = base->price(cls, left);
return left <= below ? p + p * percent / 100 : p;
}
};
struct Hold {
std::string id, passenger;
std::vector<std::string> seats;
int total, expires;
};
struct Booking {
std::string pnr, passenger;
std::vector<std::string> seats;
int total;
};
struct Refused : std::runtime_error {
using std::runtime_error::runtime_error;
};
class Flight {
public:
Flight(std::string code, const Clock& clock, const Fare& fare, int holdFor)
: code_(std::move(code)), clock_(clock), fare_(fare), holdFor_(holdFor) {}
void addRow(int row, SeatClass cls, const std::string& letters) {
std::lock_guard lock(mu_);
for (char l : letters) {
std::string no = std::to_string(row) + l;
seats_[no] = Seat{no, cls, State::Free, ""};
order_.push_back(no);
}
}
// Keeps all the seats for one passenger, or none of them.
Hold hold(const std::string& passenger, const std::vector<std::string>& nos) {
std::lock_guard lock(mu_);
releaseExpired();
if (nos.empty()) throw Refused("pick at least one seat");
std::map<SeatClass, int> left;
for (auto& [_, s] : seats_)
if (s.state == State::Free) left[s.cls]++;
int total = 0;
std::set<std::string> seen;
for (auto& no : nos) { // check every seat first, touch nothing
auto it = seats_.find(no);
if (it == seats_.end()) throw Refused("no seat " + no);
if (!seen.insert(no).second) throw Refused("seat " + no + " picked twice");
if (it->second.state != State::Free) throw Refused("seat " + no + " is " + name(it->second.state));
total += fare_.price(it->second.cls, left[it->second.cls]);
}
Hold h{"H" + std::to_string(++nextHold_), passenger, nos, total, clock_.now() + holdFor_};
for (auto& no : nos) {
seats_[no].state = State::Held;
seats_[no].hold = h.id;
}
holds_[h.id] = h;
return h;
}
Booking pay(const std::string& holdId) {
std::lock_guard lock(mu_);
releaseExpired();
auto it = holds_.find(holdId);
if (it == holds_.end()) {
auto ex = expired_.find(holdId);
if (ex != expired_.end()) throw Refused("hold " + holdId + " expired at " + hhmm(ex->second));
throw Refused("no hold " + holdId);
}
Hold h = it->second;
holds_.erase(it);
for (auto& no : h.seats) {
seats_[no].state = State::Booked;
seats_[no].hold.clear();
}
return Booking{"PNR-" + std::to_string(++nextPnr_), h.passenger, h.seats, h.total};
}
void release(const std::string& holdId) {
std::lock_guard lock(mu_);
auto it = holds_.find(holdId);
if (it == holds_.end()) throw Refused("no hold " + holdId);
free(it->second);
holds_.erase(it);
}
std::vector<Seat> seats() {
std::lock_guard lock(mu_);
releaseExpired();
std::vector<Seat> out;
for (auto& no : order_) out.push_back(seats_.at(no));
return out;
}
private:
void releaseExpired() { // callers hold the lock
int now = clock_.now();
for (auto it = holds_.begin(); it != holds_.end();) {
if (now >= it->second.expires) {
free(it->second);
expired_[it->first] = it->second.expires;
it = holds_.erase(it);
} else {
++it;
}
}
}
void free(const Hold& h) {
for (auto& no : h.seats) {
seats_[no].state = State::Free;
seats_[no].hold.clear();
}
}
std::string code_;
const Clock& clock_;
const Fare& fare_;
int holdFor_;
std::mutex mu_;
std::vector<std::string> order_;
std::unordered_map<std::string, Seat> seats_;
std::unordered_map<std::string, Hold> holds_;
std::unordered_map<std::string, int> expired_;
int nextHold_ = 0, nextPnr_ = 0;
};
struct FakeClock : Clock {
int t = 0;
int now() const override { return t; }
void set(const std::string& s) { t = std::stoi(s.substr(0, 2)) * 60 + std::stoi(s.substr(3, 2)); }
};
int main() {
FakeClock clock;
Surge fare(std::make_unique<ClassFare>(std::map<SeatClass, int>{{SeatClass::Economy, 5000}, {SeatClass::Business, 12000}}), 1, 25);
Flight f("CF-141", clock, fare, 10);
f.addRow(1, SeatClass::Business, "AB");
f.addRow(2, SeatClass::Economy, "ABC");
f.addRow(3, SeatClass::Economy, "ABC");
std::printf("flight CF-141 Dhaka -> Cox's Bazar: 2 business, 6 economy, holds last 10 min\n");
auto hold = [&](const std::string& at, const std::string& who, std::vector<std::string> seats) {
clock.set(at);
char label[64];
std::snprintf(label, sizeof label, "%s %-6s holds %s", at.c_str(), who.c_str(), join(seats).c_str());
try {
Hold h = f.hold(who, seats);
std::printf("%-28s -> %s, %d tk, pay by %s\n", label, h.id.c_str(), h.total, hhmm(h.expires).c_str());
} catch (const Refused& e) {
std::printf("%-28s -> refused: %s\n", label, e.what());
}
};
auto pay = [&](const std::string& at, const std::string& who, const std::string& id) {
clock.set(at);
char label[64];
std::snprintf(label, sizeof label, "%s %-6s pays %s", at.c_str(), who.c_str(), id.c_str());
try {
Booking b = f.pay(id);
std::printf("%-28s -> booked %s: %s\n", label, b.pnr.c_str(), join(b.seats).c_str());
} catch (const Refused& e) {
std::printf("%-28s -> refused: %s\n", label, e.what());
}
};
hold("10:00", "Rahim", {"2A", "2B"});
hold("10:01", "Nadia", {"2B", "2C"});
hold("10:01", "Nadia", {"2C", "3A"});
pay("10:03", "Rahim", "H1");
hold("10:05", "Tania", {"1A"});
pay("10:06", "Tania", "H3");
pay("10:12", "Nadia", "H2");
hold("10:12", "Sabbir", {"2C", "3A", "3B"});
hold("10:13", "Karim", {"3C"});
clock.set("10:14");
f.release("H5");
std::printf("%-28s -> 3C is free again\n", "10:14 Karim gives up H5");
std::map<State, std::vector<std::string>> by;
for (auto& s : f.seats()) by[s.state].push_back(s.no);
std::printf("10:14 booked: %s | held: %s | free: %s\n", join(by[State::Booked]).c_str(),
join(by[State::Held]).c_str(), join(by[State::Free]).c_str());
}What changes from language to language: Go uses time.Time; the other five count minutes since midnight to keep the demo short (real code would use their date-time types). Rust keeps every changing field inside the Mutex (the Inner struct), so the compiler won't let you touch seats without the lock. Java uses synchronized methods and a LinkedHashMap to keep the seat order. C++ uses std::lock_guard and an order_ vector because unordered_map has no order. TypeScript runs on one thread and a method can't be interrupted halfway, so it needs no lock.
Testing
// flight_test.go
package flights
import (
"fmt"
"strings"
"sync"
"testing"
"time"
)
type clock struct{ t time.Time }
func (c *clock) Now() time.Time { return c.t }
func newFlight(c *clock) *Flight {
f := NewFlight("T-1", c, Surge{ClassFare{Economy: 5000, Business: 12000}, 1, 25}, 10*time.Minute)
f.AddRow(1, Business, "AB")
f.AddRow(2, Economy, "ABC")
f.AddRow(3, Economy, "ABC")
return f
}
func state(f *Flight, no string) State {
for _, s := range f.Seats() {
if s.No == no {
return s.State
}
}
panic("no seat " + no)
}
func TestHoldIsAllOrNothing(t *testing.T) {
f := newFlight(&clock{})
if _, err := f.Hold("Rahim", "2B"); err != nil {
t.Fatal(err)
}
if _, err := f.Hold("Nadia", "2A", "2B"); err == nil {
t.Fatal("2B is held, the whole hold should be refused")
}
if state(f, "2A") != Free {
t.Fatal("a refused hold must not keep 2A")
}
}
func TestExpiredHoldIsRefusedAndSeatsComeBack(t *testing.T) {
c := &clock{}
f := newFlight(c)
h, _ := f.Hold("Nadia", "2C")
c.t = c.t.Add(10 * time.Minute) // exactly at the deadline: too late
if _, err := f.Pay(h.ID); err == nil || !strings.Contains(err.Error(), "expired") {
t.Fatalf("want expired, got %v", err)
}
if _, err := f.Hold("Sabbir", "2C"); err != nil {
t.Fatalf("2C should be free again: %v", err)
}
}
func TestPayBooksSeatsOnce(t *testing.T) {
f := newFlight(&clock{})
h, _ := f.Hold("Rahim", "2A", "2B")
b, err := f.Pay(h.ID)
if err != nil || b.Total != 10000 || state(f, "2A") != Booked {
t.Fatalf("booking %+v, err %v", b, err)
}
if _, err := f.Pay(h.ID); err == nil {
t.Fatal("paying the same hold twice must fail")
}
if _, err := f.Hold("Nadia", "2A"); err == nil {
t.Fatal("a booked seat can't be held")
}
}
func TestReleaseFreesSeatsAtOnce(t *testing.T) {
f := newFlight(&clock{})
h, _ := f.Hold("Karim", "3C")
if err := f.Release(h.ID); err != nil || state(f, "3C") != Free {
t.Fatalf("err %v, 3C is %v", err, state(f, "3C"))
}
}
func TestLastSeatCostsMore(t *testing.T) {
f := newFlight(&clock{})
h1, _ := f.Hold("A", "1A") // 2 business seats free: normal price
h2, _ := f.Hold("B", "1B") // only 1 left: +25%
if h1.Total != 12000 || h2.Total != 15000 {
t.Fatalf("got %d and %d", h1.Total, h2.Total)
}
}
func TestBadSeatsRefused(t *testing.T) {
f := newFlight(&clock{})
for _, seats := range [][]string{{"9Z"}, {"2A", "2A"}, {}} {
if _, err := f.Hold("X", seats...); err == nil {
t.Fatalf("%v should be refused", seats)
}
}
}
// 100 people press "hold 2A" in the same instant. Exactly one may win. Run with -race.
func TestOneSeatManyBuyers(t *testing.T) {
f := newFlight(&clock{})
var wg sync.WaitGroup
var mu sync.Mutex
wins := 0
for i := range 100 {
wg.Add(1)
go func() {
defer wg.Done()
if _, err := f.Hold(fmt.Sprint("p", i), "2A"); err == nil {
mu.Lock()
wins++
mu.Unlock()
}
}()
}
wg.Wait()
if wins != 1 {
t.Fatalf("%d people got seat 2A", wins)
}
}
// Overlapping pairs held and paid from many goroutines: no seat ends up in two bookings.
func TestNoSeatBookedTwice(t *testing.T) {
f := newFlight(&clock{})
all := []string{"1A", "1B", "2A", "2B", "2C", "3A", "3B", "3C"}
var wg sync.WaitGroup
var mu sync.Mutex
owner := map[string]string{}
for i := range 200 {
wg.Add(1)
go func() {
defer wg.Done()
pair := []string{all[i%8], all[(i+1)%8]}
h, err := f.Hold(fmt.Sprint("p", i), pair...)
if err != nil {
return
}
b, err := f.Pay(h.ID)
if err != nil {
return
}
mu.Lock()
defer mu.Unlock()
for _, no := range b.Seats {
if prev, taken := owner[no]; taken {
t.Errorf("seat %s booked by %s and %s", no, prev, b.PNR)
}
owner[no] = b.PNR
}
}()
}
wg.Wait()
}
$ go test -race -v ./...
=== RUN TestHoldIsAllOrNothing
--- PASS: TestHoldIsAllOrNothing (0.00s)
=== RUN TestExpiredHoldIsRefusedAndSeatsComeBack
--- PASS: TestExpiredHoldIsRefusedAndSeatsComeBack (0.00s)
=== RUN TestPayBooksSeatsOnce
--- PASS: TestPayBooksSeatsOnce (0.00s)
=== RUN TestReleaseFreesSeatsAtOnce
--- PASS: TestReleaseFreesSeatsAtOnce (0.00s)
=== RUN TestLastSeatCostsMore
--- PASS: TestLastSeatCostsMore (0.00s)
=== RUN TestBadSeatsRefused
--- PASS: TestBadSeatsRefused (0.00s)
=== RUN TestOneSeatManyBuyers
--- PASS: TestOneSeatManyBuyers (0.00s)
=== RUN TestNoSeatBookedTwice
--- PASS: TestNoSeatBookedTwice (0.00s)
PASS
ok flights 1.019s
? flights/cmd/demo [no test files]
| Test | Checks |
|---|---|
TestHoldIsAllOrNothing | If 2B is taken, a hold for 2A + 2B is refused and 2A stays free |
TestExpiredHoldIsRefusedAndSeatsComeBack | At exactly 10 minutes the payment is refused and someone else can hold the seat |
TestPayBooksSeatsOnce | Paying books the seats; paying the same hold again fails; a booked seat can't be held |
TestReleaseFreesSeatsAtOnce | Giving up frees the seat immediately |
TestLastSeatCostsMore | The second-to-last business seat is 12000, the last is 15000 |
TestBadSeatsRefused | Unknown seat, the same seat twice, and no seats are refused |
TestOneSeatManyBuyers | 100 goroutines hold 2A at once: exactly one wins |
TestNoSeatBookedTwice | 200 goroutines hold and pay overlapping pairs: no seat ends up in two bookings |
We deleted the lock lines from flight.go and ran the tests again. The race detector reported data races on every run (15 to 23 in our runs), and TestNoSeatBookedTwice caught real double sales:
flight_test.go:146: seat 1A booked by PNR-2 and PNR-4
flight_test.go:146: seat 1B booked by PNR-1 and PNR-4
Design review
What's good: a seat's life is small and explicit; holds can't leak because expiry runs before every operation; time and pricing are injected, so tests are instant and exact.
| "What if…" | What to change |
|---|---|
| A real payment gateway | Paying takes seconds; don't call it under the lock. Mark the hold "paying" under the lock, call the gateway without it, then take the lock again to book (or to free the seats if the payment failed). Extend the deadline while a payment is in flight. |
| Many servers | An in-memory lock only protects one process. Put seats in a database and hold with one conditional update (UPDATE seats SET state='held' WHERE id IN (…) AND state='free', then check the row count), or use row locks inside a transaction. |
| Thousands of flights | One Flight per flight already gives you one lock per flight, so different flights never wait for each other. |
| The expired map grows forever | Keep expired ids for a day, or store holds with a status in the database instead. |
| Cancel a booking | Add booked → free with a refund rule; that's a new transition, and probably a Refund strategy. |
| Overbooking (airlines really do it) | Allow holds up to capacity + N for economy; it becomes a business rule in Hold, not a change to the seat states. |
Interview questions
Practice questions
Beginner
- What states can a seat be in, and what moves it between them?
- Why hold seats at all, instead of booking on click?
Intermediate
- How do you make sure a family's three seats are held together or not at all?
- How do you test a 10-minute timeout without waiting 10 minutes?
Advanced
- Two people press "hold 2A" in the same millisecond. Walk through what happens with and without the lock.
- The payment gateway takes 5 seconds. Where does the lock go?
Common design mistakes
- Checking and marking in separate steps, so two buyers both see "free".
- Holding seats one by one, leaving half a family's seats held when one fails.
- Holds that never expire, so abandoned carts empty the flight.
- Calling
time.Now()everywhere, which makes expiry impossible to test. - Calling the payment gateway while holding the lock, so one slow payment freezes the whole flight.
Answers
1. A seat's states?
Free, held, booked. Hold moves free → held; pay moves held → booked; expiry or release moves held → free. Nothing leaves booked here.
2. Why hold first?
Paying takes minutes. Without a hold, someone else could buy the seat while you type your card number. The hold keeps it for you; the timeout makes sure it comes back if you leave.
3. Three seats together?
Check all three first (they exist, aren't repeated, are free) without changing anything. Only if all pass, mark all three held. Both steps run under one lock, so nobody can sneak in between.
4. Testing a 10-minute timeout?
Pass in a Clock. The test uses a fake one and moves it forward 10 minutes, then calls Pay. The flight sees the new time and frees the hold. The test takes microseconds.
5. Same millisecond, with and without the lock?
With the lock, one goroutine checks and marks 2A held; the other waits, then sees "held" and is refused. Without it, both read "free" before either writes, and both get the seat. In Go that's also a data race on the map, which can crash the program. Without the lock, our test caught seat 1A booked under two PNRs.
6. Slow payment gateway?
Never call it while holding the lock. Under the lock, mark the hold as paying; release the lock; call the gateway; take the lock again and book the seats or free them. The hold's state stops other buyers in the meantime.
What interviewers look for: clear seat states, holds with a deadline, all-or-nothing, check-and-change as one step, testable time, and knowing what changes with real payments and many servers.
Designed it yourself and understood the solution? Mark it to track your progress.