Practice · Low Level Design · Shared State and Contention
Design a Flight Booking System
দুইজন, একটা seat, একই সেকেন্ড: দশ মিনিটের জন্য seat hold করা, টাকা না দিলে seat ফেরত দেওয়া, আর একটা seat কখনো দুইবার বিক্রি না হওয়া।
Learn mode: পুরো solution নিচে দেখানো আছে।
Problem এর পরিচয়
| Difficulty | Intermediate |
| সময় | Design আর code মিলিয়ে ২–৩ ঘণ্টা; interview তে মোটামুটি ৪৫ মিনিট |
| আগে জানা দরকার | Interface, map, lock |
| যা শিখবে | Seat এর state (free, held, booked), সময় শেষ হলে বাতিল হওয়া hold, বাইরে থেকে দেওয়া clock, হয় সব seat নয় একটাও না, দামের জন্য Strategy আর তার উপর Decorator, check আর change এক lock এ |
| বাস্তবে কোথায় | Airline আর bus এর ticket site, সিনেমা আর concert এর seat, ঈদের train ticket, যেকোনো "আগে seat বাছো, পরে টাকা দাও" flow |
Flight booking হলো "সবাই একই জিনিস চায়" ধরনের classic প্রশ্ন। Seat বাছা আর টাকা দেওয়ার মাঝে কয়েক মিনিট চলে যায়, আর ওই সময়ে হাজার জন একই seat map দেখছে। Design কে তিনটা প্রশ্নের উত্তর দিতে হয়: টাকা দেওয়ার সময়টুকু seat টা তোমার জন্য কীভাবে আটকে থাকবে, টাকা না দিলে কী হবে, আর দুইজন একই millisecond এ একই seat এ click করলে দুজনেই যেন না পায়।
Problem statement
CF-141 flight টা ঢাকা থেকে কক্সবাজার যায়। এতে ২টা business seat (row 1) আর ৬টা economy seat (row 2 আর 3)। ঈদের আগে সবাই একসাথে app খোলে।
একজন passenger এক বা একাধিক seat বাছে। Seat গুলো ১০ মিনিটের জন্য hold হয়ে যায়, যাতে সে টাকা দেওয়ার সময় অন্য কেউ নিতে না পারে। সময়ের মধ্যে টাকা দিলে seat গুলো booked হয় আর সে একটা PNR পায়। টাকা না দিলে, বা app বন্ধ করে চলে গেলে, seat গুলো আবার বিক্রির জন্য খুলে যায়।
আমরা কী বানাচ্ছি: একটা flight এর booking এর মূল অংশ: seat map, hold, payment, seat ছেড়ে দেওয়া, আর দাম।
কেন জরুরি: দুইজন একই seat পেলে একজন gate এ ticket হাতে দাঁড়িয়ে থাকবে, কিন্তু বসার জায়গা নেই। আর ছেড়ে যাওয়া hold যদি কখনো ফেরত না আসে, তাহলে app এ flight full দেখাবে কিন্তু plane উড়বে অর্ধেক খালি।
এক লাইনে: একজন passenger কে অল্প সময়ের জন্য seat hold করতে দাও, টাকা দিলে hold টাকে booking বানাও, না দিলে seat ফেরত দাও, আর একটা seat কখনো দুইবার বিক্রি কোরো না।
Requirement
Functional requirement
- একটা flight এ অনেকগুলো seat; প্রতিটা seat এর একটা নম্বর (2A) আর একটা class (business, economy)।
- Hold(passenger, seats): বাছা সব seat ১০ মিনিটের জন্য আটকে রাখো, আর কোনো একটা নেওয়া থাকলে একটাও না।
- Pay(hold): চালু থাকা hold কে PNR সহ booking বানাও। সময় শেষ হয়ে যাওয়া hold এ টাকা নেওয়া হবে না।
- Release(hold): seat গুলো সাথে সাথে ফেরত দাও (passenger আর নিচ্ছে না)।
- Hold এর সময় শেষ হলে seat গুলো নিজে থেকেই আবার free হয়ে যায়, কাউকে কিছু করতে হয় না।
- প্রতিটা seat এর দাম তার class অনুযায়ী; কোনো class এর শেষ free seat টার দাম ২৫% বেশি।
- যে seat নেই, আর একই seat দুইবার বাছলে, সেটা ফিরিয়ে দাও।
Non-functional requirement
| Requirement | এখানে মানে কী |
|---|---|
| কখনো দুইবার বিক্রি না | দুইজন একই মুহূর্তে একই seat hold করলে ঠিক একজন পাবে |
| Seat ফেরত আসে | টাকা না দেওয়া hold সময়মতো seat ছেড়ে দেয়, যাতে আবার বিক্রি করা যায় |
| নতুন rule যোগ করা সহজ | নতুন দামের rule (ঈদের surge, student discount) booking এর code ছোঁয় না |
| Test করা যায় | সময় আসে বাইরে থেকে দেওয়া একটা clock থেকে, তাই test অপেক্ষা না করেই ১০ মিনিট এগিয়ে যেতে পারে |
যা ধরে নিচ্ছি আর যা এখানে নেই
- একটা flight, একটা currency (টাকা)। Payment একটা call, যেটা সবসময় সফল হয়; আসল gateway নেই, refund নেই, booking cancel নেই, passenger এর details নেই।
যা জমা দিতে হবে
- Entity গুলো, seat এর state diagram, আর lock টা কোথায়।
- Hold, pay, release আর সময় শেষ হওয়ার code।
- হয়-সব-নয়-একটাও-না hold, সময় শেষ, দাম আর concurrency এর test।
Hint 1: একটা seat এর জীবন
একটা seat কোন কোন state এ থাকতে পারে আর কী হলে এক state থেকে আরেকটায় যায়, লিখে ফেলো। একটা state থেকে পেছনে ফেরার রাস্তা আছে।
Hint 2: দুইটা seat, একটা নেওয়া
কেউ 2A আর 2B hold করতে চাইল, কিন্তু 2B আগেই নেওয়া। তার কি 2A টা রাখা উচিত? পরিবার নিয়ে গেলে মানুষ পাশাপাশি seat চায়। কিছু বদলানোর আগে সব seat check করো।
Hint 3: সময় শেষ হওয়া hold কে ছাড়বে?
একটা background timer চালাতে পারো, অথবা প্রতিটা কাজের শুরুতে দেখে নিতে পারো কোন hold এর সময় শেষ। দ্বিতীয়টায় আলাদা goroutine লাগে না।
Hint 4: একই millisecond
"Seat টা কি free?" আর "এটাকে held করো", এই দুইটা এক ধাপে হতে হবে। মাঝখানে অন্য goroutine চলার সুযোগ পেলে দুজনেই "free" দেখবে।
মূল entity
| Entity | এক কথায় কাজ | কী রাখে | কী করে |
|---|---|---|---|
Seat | একটা seat আর সে এখন কোন অবস্থায় | নম্বর, class, state, কোন hold এর কাছে আছে | কিছু না: শুধু data |
State | Seat এর state | free / held / booked | কিছু না: একটা enum |
Hold | একজন passenger এর জন্য একটা deadline পর্যন্ত আটকে রাখা seat | id, passenger, seats, total, expires | কিছু না: একটা value |
Booking | টাকা দেওয়া reservation | PNR, passenger, seats, total | কিছু না: একটা value |
Fare (interface) | একটা seat এর দাম বলে | rule এর উপর নির্ভর করে | Price(class, left) |
ClassFare, Surge | Class অনুযায়ী ঠিক করা দাম; আর একটা wrapper যেটা শেষ seat গুলোর দামে percentage যোগ করে | দাম; base fare, সীমা, percent | Price |
Clock (interface) | সময় বলে | কিছু না | Now() |
Flight | ঢোকার দরজা; seat, hold আর lock এর মালিক | map এর ক্রমে seat, চালু hold, বাতিল hold, counter | Hold, Pay, Release, Seats |
কে কার সাথে কীভাবে যুক্ত: Flight তার seat আর hold গুলোর মালিক। সে একটা Fare (Strategy) আর একটা Clock ব্যবহার করে, দুটোই বাইরে থেকে দেওয়া। Surge আরেকটা Fare কে মুড়ে রাখে (Decorator)। একটা Seat তার hold কে চেনে id দিয়ে, pointer দিয়ে না, যাতে সত্যি তথ্য থাকে এক জায়গাতেই: flight এর hold এর map এ।
Diagram
Class diagram
কীভাবে পড়বে: Seat আর Hold এর মালিক Flight, আর কেউ টাকা দিলে সে একটা Booking বানায়। তাকে দুইটা interface দেওয়া হয়: দামের জন্য Fare আর সময়ের জন্য Clock, এ জন্যই test দুটোকেই নিজের হাতে চালাতে পারে। ClassFare আর Surge দুটোই Fare implement করে; Surge থেকে Fare এর দিকে ফাঁপা diamond টাই Decorator: একটা Surge এর ভেতরে আরেকটা Fare থাকে, আর সে তার দামের উপর যোগ করে। ফাঁপা diamond ◇ হলো একটু ঢিলা has a: Surge যে Fare ধরে রাখে সেটা বাইরে তৈরি, তাকে ছাড়াও বাঁচতে পারে।
চিহ্ন গুলো: + মানে public, - মানে private। ভরা diamond ◆ মানে মালিক (অংশটা মালিকের সাথেই আসে, সাথেই যায়), সাধারণ তীর মানে reference ধরে রাখে, dashed তীর মানে ব্যবহার করে, আর ফাঁপা ত্রিভুজওয়ালা dashed তীর মানে এই interface implement করে।
Sequence: একটা hold, তারপর দেরিতে payment
কীভাবে পড়বে: প্রতিটা খাড়া dashed লাইন একটা object, আর সময় চলে উপর থেকে নিচে। ভরা তীর মানে call, dashed তীর মানে উত্তর ফেরত আসা, আর সরু bar গুলো দেখায় কোন object কখন কাজে ব্যস্ত। দুইটা call ই releaseExpired() দিয়ে শুরু হয়, তাই কেউ পড়ার আগে seat map সবসময় পরিষ্কার থাকে। খেয়াল করো, 10:11 এ কেউ Flight কে ডাকে না: hold টা শুধু অচল হয়ে যায়, আর পরের যে call আসে সে সেটা টের পায়।
একটা seat এর জীবন
কীভাবে পড়বে: কেউ hold করলে seat free → held হয়, টাকা দিলে held → booked। ফেরার একটাই রাস্তা, held → free: সময় শেষ, বা passenger ছেড়ে দিয়েছে। এই design এ booked seat আর ফেরত যায় না (booking cancel এখানে নেই)।
একটা seat, দুইজন ক্রেতা
কীভাবে পড়বে: Lock এর কারণে "সব seat check করো" আর "সেগুলো held করো" এক ধাপ হয়ে যায়। যে দ্বিতীয়তে lock পায়, সে প্রথম জনের বদলটা দেখে। Lock না থাকলে দুজনেই কেউ "held" লেখার আগেই "free" পড়ে ফেলতে পারে, আর দুজনেই 2A নিয়ে চলে যায়।
Design principle
- হয় সব, নয় একটাও না:
Holdকোনো seat বদলানোর আগে সবগুলো check করে (আছে কিনা, দুইবার বাছা হয়নি, free কিনা)। না বলা hold কোনো দাগ রেখে যায় না। - সময় বাইরে থেকে আসে:
Flightনিজে কখনোtime.Now()ডাকে না। তাকে যেClockদেওয়া হয়েছে, তাকে জিজ্ঞেস করে, তাই এখন কয়টা বাজে সেটা test আর demo ঠিক করে। - দরকারের সময় বাতিল করো: প্রতিটা public method শুরুতেই সময় শেষ হওয়া hold গুলো ছেড়ে দেয়। কোনো background goroutine নেই, আর পুরনো hold এ টাকা দেওয়ার কোনো ফাঁকও নেই।
- Check আর change এক lock এ: seat আর hold একসাথে বদলায়, তাই দুটো একই lock ভাগ করে।
- Open/closed: নতুন দামের rule মানে নতুন একটা
Fare;Flightবদলায় না।
Design pattern
State (seat এর জন্য)
তিনটা state আর চারটা transition: free → held (hold), held → booked (pay), held → free (সময় শেষ বা release)। একটা enum আর Flight এর ভেতরের check গুলোই যথেষ্ট। প্রতিটা state এর নিজের অনেক কাজ থাকলে তখন আলাদা state class এর মানে হতো।
Strategy (দামের জন্য)
- কেন: ভাড়া সবসময় বদলায়: class দিয়ে, flight কতটা ভরা তা দিয়ে, season দিয়ে, promo code দিয়ে।
- কী সমাধান করে: দাম ঠিক করা একটাই বদলানো যায় এমন object,
Fare। - না থাকলে:
Holdএর ভেতরেif eid { … } else if lastSeats { … }বাড়তেই থাকে। - Trade-off: একটা interface; খরচ প্রায় নেই।
- কোথায়:
fare.go;Holdডাকেf.fare.Price(class, left)।
Decorator (surge এর জন্য)
Surge নিজেও একটা Fare, আর ভেতরে আরেকটা Fare রাখে। Base এর কাছে দাম চায়, আর class প্রায় ভরে গেলে ২৫% যোগ করে। এগুলো একটার উপর আরেকটা বসানো যায়: class fare এর উপর surge, তার উপর student discount, আর Flight তবুও একটাই Fare দেখে।
Go তে পুরো solution
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
ধাপ ১: seat আর তার state
// 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
}
ধাপ ২: দাম
left মানে এই hold এর আগে ওই class এর কয়টা seat free ছিল। এটা বাইরে থেকে দেওয়ায় দামের rule গুলো পরিষ্কার থাকে: তারা কখনো 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
}
ধাপ ৩: flight
Hold টা দুই ভাগে পড়ো। প্রথম loop শুধু check করে আর দাম যোগ করে। দ্বিতীয় loop চলে কেবল সব ঠিক থাকলে, আর তখনই seat বদলায়। দুই ভাগই একই 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, ""
}
}
ধাপ ৪: চালিয়ে দেখো
// 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
Output টা পড়ি:
- 10:00: Rahim 2A আর 2B hold করে: দুইটা economy seat, ৫০০০ করে, deadline 10:10।
- 10:01: Nadia চায় 2B আর 2C। 2B তো Rahim এর, তাই পুরো hold টাই না, আর 2C free থেকে যায় (এটাই হয়-সব-নয়-একটাও-না)। তারপর সে 2C আর 3A hold করে।
- 10:03: Rahim সময়মতো টাকা দিয়ে PNR-1 পায়। Hold id আর PNR আলাদা counter, তাই Tania এর hold H3 হলেও তার booking PNR-2।
- 10:12: Nadia টাকা দিতে যায়, কিন্তু তার deadline ছিল 10:11। 10:11 এর পরে কেউ flight টা ছুঁতেই তার seat গুলো ছেড়ে দেওয়া হয়েছে, তাই payment না।
- 10:12: Sabbir নেয় 2C, 3A আর 3B: Nadia যেগুলো হারাল, সাথে আরেকটা। তখন ৪টা economy seat free ছিল, তাই surge নেই: ১৫০০০।
- 10:13: Karim hold করে 3C, শেষ economy seat: ৫০০০ + ২৫% = ৬২৫০।
- 10:14: Karim ছেড়ে দেয়, আর 3C সাথে সাথে আবার free। শেষ লাইনটা seat map।
বাকি পাঁচটা language
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());
}Language ভেদে কী আলাদা: Go তে time.Time; বাকি পাঁচটায় demo ছোট রাখতে মাঝরাত থেকে কত মিনিট গেছে সেটা গোনা হয়েছে (আসল code এ তাদের নিজেদের date-time type লাগবে)। Rust এ যা যা বদলায় সব Mutex এর ভেতরে (Inner struct), তাই lock ছাড়া seat ছুঁতে গেলে compiler ই আটকে দেয়। Java তে synchronized method আর seat এর ক্রম রাখতে LinkedHashMap। C++ এ std::lock_guard, আর unordered_map এ কোনো ক্রম থাকে না বলে আলাদা একটা order_ vector। TypeScript এক thread এ চলে আর একটা method মাঝপথে থামে না, তাই সেখানে 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 | কী check করে |
|---|---|
TestHoldIsAllOrNothing | 2B নেওয়া থাকলে 2A + 2B এর hold না, আর 2A free থাকে |
TestExpiredHoldIsRefusedAndSeatsComeBack | ঠিক ১০ মিনিটে payment না, আর অন্য কেউ seat টা hold করতে পারে |
TestPayBooksSeatsOnce | টাকা দিলে seat booked; একই hold এ আবার টাকা দিলে fail; booked seat hold করা যায় না |
TestReleaseFreesSeatsAtOnce | ছেড়ে দিলে seat সাথে সাথে free |
TestLastSeatCostsMore | শেষের আগের business seat ১২০০০, শেষটা ১৫০০০ |
TestBadSeatsRefused | অচেনা seat, একই seat দুইবার, আর কোনো seat না দিলে না |
TestOneSeatManyBuyers | ১০০টা goroutine একসাথে 2A hold করে: ঠিক একজন পায় |
TestNoSeatBookedTwice | ২০০টা goroutine একটার সাথে আরেকটা মিলে যায় এমন seat জোড়া hold করে টাকা দেয়: কোনো seat দুইটা booking এ যায় না |
আমরা flight.go থেকে lock এর লাইনগুলো মুছে আবার test চালিয়েছি। প্রতিবার race detector data race ধরেছে (আমাদের run এ ১৫ থেকে ২৩টা), আর TestNoSeatBookedTwice সত্যি সত্যি দুইবার বিক্রি ধরে ফেলেছে:
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
যা ভালো: seat এর জীবন ছোট আর পরিষ্কার; প্রতিটা কাজের আগে বাতিলের কাজ চলে, তাই hold আটকে থাকতে পারে না; সময় আর দাম বাইরে থেকে আসে, তাই test সাথে সাথে শেষ হয় আর একদম ঠিকঠাক।
| "যদি…" | কী বদলাতে হবে |
|---|---|
| আসল payment gateway | Payment এ কয়েক সেকেন্ড লাগে; lock ধরে রেখে gateway ডেকো না। Lock এর ভেতরে hold টাকে "paying" mark করো, lock ছেড়ে gateway ডাকো, তারপর আবার lock নিয়ে booking করো (payment fail হলে seat ছেড়ে দাও)। Payment চলার সময় deadline বাড়িয়ে দাও। |
| অনেকগুলো server | Memory র lock শুধু একটা process কে বাঁচায়। Seat গুলো database এ রাখো আর একটা শর্তওয়ালা update দিয়ে hold করো (UPDATE seats SET state='held' WHERE id IN (…) AND state='free', তারপর কয়টা row বদলাল দেখো), অথবা transaction এর ভেতরে row lock নাও। |
| হাজারটা flight | প্রতিটা flight এর জন্য একটা করে Flight, মানে প্রতিটা flight এর নিজের lock, তাই এক flight আরেক flight এর জন্য অপেক্ষা করে না। |
| Expired map বাড়তেই থাকে | বাতিল id গুলো এক দিন রাখো, অথবা database এ hold গুলো status সহ রাখো। |
| Booking cancel | Refund এর rule সহ booked → free যোগ করো; এটা একটা নতুন transition, আর সম্ভবত একটা Refund strategy। |
| Overbooking (airline রা আসলেই করে) | Economy তে capacity + N পর্যন্ত hold করতে দাও; এটা Hold এর ভেতরের একটা business rule হয়, seat এর state বদলায় না। |
Interview প্রশ্ন
Practice প্রশ্ন
Beginner
- একটা seat কোন কোন state এ থাকতে পারে, আর কী হলে এক state থেকে আরেকটায় যায়?
- Click করলেই book না করে আগে hold কেন?
Intermediate
- একটা পরিবারের তিনটা seat হয় একসাথে hold হবে নয় একটাও না, এটা কীভাবে করবে?
- ১০ মিনিটের timeout ১০ মিনিট অপেক্ষা না করে কীভাবে test করবে?
Advanced
- দুইজন একই millisecond এ "hold 2A" চাপল। Lock সহ আর lock ছাড়া কী হয়, ধাপে ধাপে বলো।
- Payment gateway ৫ সেকেন্ড নেয়। Lock কোথায় বসবে?
Design এর সাধারণ ভুল
- Check আর mark আলাদা ধাপে করা, ফলে দুজন ক্রেতাই "free" দেখে।
- একটা একটা করে seat hold করা, ফলে একটা fail করলে পরিবারের অর্ধেক seat আটকে থাকে।
- Hold এর কোনো মেয়াদ না রাখা, ফলে ছেড়ে যাওয়া cart গুলো flight খালি করে দেয়।
- সব জায়গায়
time.Now()ডাকা, এতে সময় শেষ হওয়া test করাই যায় না। - Lock ধরে রেখে payment gateway ডাকা, ফলে একটা ধীর payment পুরো flight আটকে দেয়।
উত্তর
১. Seat এর state?
Free, held, booked। Hold করলে free → held; টাকা দিলে held → booked; সময় শেষ বা release হলে held → free। এখানে booked থেকে কিছু বের হয় না।
২. আগে hold কেন?
টাকা দিতে কয়েক মিনিট লাগে। Hold না থাকলে তুমি card নম্বর টাইপ করতে করতে অন্য কেউ seat টা কিনে ফেলতে পারে। Hold seat টা তোমার জন্য আটকে রাখে; আর timeout থাকায় তুমি চলে গেলে seat ফেরত আসে।
৩. একসাথে তিনটা seat?
আগে তিনটাই check করো (আছে, repeat হয়নি, free), কিছু না বদলে। সব ঠিক থাকলে তবেই তিনটাকে held করো। দুই ধাপই এক lock এর ভেতরে, তাই মাঝখানে কেউ ঢুকতে পারে না।
৪. ১০ মিনিটের timeout এর test?
একটা Clock বাইরে থেকে দাও। Test একটা নকল clock নিয়ে সেটাকে ১০ মিনিট এগিয়ে দেয়, তারপর Pay ডাকে। Flight নতুন সময় দেখে hold টা ছেড়ে দেয়। Test শেষ হয় microsecond এ।
৫. একই millisecond, lock সহ আর lock ছাড়া?
Lock থাকলে একটা goroutine check করে 2A কে held করে; অন্যটা অপেক্ষা করে, তারপর "held" দেখে না শোনে। Lock ছাড়া দুজনেই কেউ লেখার আগে "free" পড়ে, আর দুজনেই seat পায়। Go তে এটা map এর উপর data race ও, যেটায় program crash করতে পারে। Lock ছাড়া আমাদের test 1A কে দুইটা PNR এ booked অবস্থায় ধরেছে।
৬. ধীর payment gateway?
Lock ধরে রেখে কখনো ডেকো না। Lock এর ভেতরে hold টাকে paying mark করো; lock ছাড়ো; gateway ডাকো; আবার lock নিয়ে seat book করো বা ছেড়ে দাও। এর মধ্যে hold এর state ই অন্য ক্রেতাদের আটকে রাখে।
Interviewer রা যা দেখেন: seat এর state পরিষ্কার, deadline সহ hold, হয়-সব-নয়-একটাও-না, check আর change এক ধাপে, test করা যায় এমন সময়, আর আসল payment বা অনেক server এলে কী বদলায় সেটা জানা।
নিজে design করে solution টা বুঝেছো? Mark করো, তাহলে progress ট্র্যাক হবে।