সব problem (16)
System Design courseSystem Design Problems

0%

0/16 done

Practice · Low Level Design · Shared State and Contention

Design a Flight Booking System

IntermediateSeat holdTimeoutsConcurrencyPricing⏱ 2-3 ঘণ্টা

দুইজন, একটা seat, একই সেকেন্ড: দশ মিনিটের জন্য seat hold করা, টাকা না দিলে seat ফেরত দেওয়া, আর একটা seat কখনো দুইবার বিক্রি না হওয়া।

Learn mode: পুরো solution নিচে দেখানো আছে।

Problem এর পরিচয়

DifficultyIntermediate
সময়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

  1. একটা flight এ অনেকগুলো seat; প্রতিটা seat এর একটা নম্বর (2A) আর একটা class (business, economy)।
  2. Hold(passenger, seats): বাছা সব seat ১০ মিনিটের জন্য আটকে রাখো, আর কোনো একটা নেওয়া থাকলে একটাও না।
  3. Pay(hold): চালু থাকা hold কে PNR সহ booking বানাও। সময় শেষ হয়ে যাওয়া hold এ টাকা নেওয়া হবে না।
  4. Release(hold): seat গুলো সাথে সাথে ফেরত দাও (passenger আর নিচ্ছে না)।
  5. Hold এর সময় শেষ হলে seat গুলো নিজে থেকেই আবার free হয়ে যায়, কাউকে কিছু করতে হয় না।
  6. প্রতিটা seat এর দাম তার class অনুযায়ী; কোনো class এর শেষ free seat টার দাম ২৫% বেশি।
  7. যে 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
StateSeat এর statefree / held / bookedকিছু না: একটা enum
Holdএকজন passenger এর জন্য একটা deadline পর্যন্ত আটকে রাখা seatid, passenger, seats, total, expiresকিছু না: একটা value
Bookingটাকা দেওয়া reservationPNR, passenger, seats, totalকিছু না: একটা value
Fare (interface)একটা seat এর দাম বলেrule এর উপর নির্ভর করেPrice(class, left)
ClassFare, SurgeClass অনুযায়ী ঠিক করা দাম; আর একটা wrapper যেটা শেষ seat গুলোর দামে percentage যোগ করেদাম; base fare, সীমা, percentPrice
Clock (interface)সময় বলেকিছু নাNow()
Flightঢোকার দরজা; seat, hold আর lock এর মালিকmap এর ক্রমে seat, চালু hold, বাতিল hold, counterHold, Pay, Release, Seats

কে কার সাথে কীভাবে যুক্ত: Flight তার seat আর hold গুলোর মালিক। সে একটা Fare (Strategy) আর একটা Clock ব্যবহার করে, দুটোই বাইরে থেকে দেওয়া। Surge আরেকটা Fare কে মুড়ে রাখে (Decorator)। একটা Seat তার hold কে চেনে id দিয়ে, pointer দিয়ে না, যাতে সত্যি তথ্য থাকে এক জায়গাতেই: flight এর hold এর map এ।

Diagram

Class diagram

Seat+ No: string+ Class: Class+ State: State+ Hold: stringFlight- seats: map[string]*Seat- holds: map[string]*Hold- clock: Clock- fare: Fare- holdFor: time.Duration- mu: sync.Mutex+ Hold(who, seats...) (Hold, error)+ Pay(holdID) (Booking, error)+ Release(holdID) error+ Seats() []Seat- releaseExpired()Hold+ ID, Passenger: string+ Seats: []string+ Total: int+ Expires: time.TimeBooking+ PNR, Passenger: string+ Seats: []string+ Total: int«interface»Fare+ Price(c, left) int«interface»Clock+ Now() time.TimeClassFare(a map[Class]int)+ Price(c, left) intSurge+ Base: Fare+ Below, Percent: int+ Price(c, left) intseats1*holdscreatesfareclockbaseFlight owns its seats and holds; one lock guards bothPay turns a live Hold into a Bookingthe fare rule and the clock are interfaces, passed inClassFare and Surge implement Fare (Strategy)Surge holds another Fare and adds to its price (Decorator)
Play চাপো। আগে Flight আর তার নিজের seat আর hold গুলো, তারপর Pay যে Booking বানায় সেটা, তারপর তাকে দেওয়া দুইটা interface, তারপর Fare এর পেছনের দামের rule গুলো, শেষে Surge কীভাবে আরেকটা Fare কে মুড়ে রাখে।

কীভাবে পড়বে: 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

PassengerFlightClockFareHold(Nadia, 2C, 3A)releaseExpired()Now()10:01all seats free?Price(economy, 4) ×25000 eachH2, 10000 tk, pay by 10:11Pay(H2) at 10:12H2 timed outerror: H2 expired at 10:11
Play চাপো। 10:01 এ Nadia 2C আর 3A hold করে: Flight পুরনো hold সরায়, clock কে সময় জিজ্ঞেস করে, seat check করে আর fare কে দাম জিজ্ঞেস করে। 10:12 তে সে টাকা দেয়, কিন্তু তার hold 10:11 তেই শেষ, তাই payment না।

কীভাবে পড়বে: প্রতিটা খাড়া dashed লাইন একটা object, আর সময় চলে উপর থেকে নিচে। ভরা তীর মানে call, dashed তীর মানে উত্তর ফেরত আসা, আর সরু bar গুলো দেখায় কোন object কখন কাজে ব্যস্ত। দুইটা call ই releaseExpired() দিয়ে শুরু হয়, তাই কেউ পড়ার আগে seat map সবসময় পরিষ্কার থাকে। খেয়াল করো, 10:11 এ কেউ Flight কে ডাকে না: hold টা শুধু অচল হয়ে যায়, আর পরের যে call আসে সে সেটা টের পায়।

একটা seat এর জীবন

holdpaytime up / give upFreeanyone can take itHeldone passenger, 10 minBookedpaid2C10:01 Nadia holds 2C: pay by 10:1110:11: no payment, the seat comes back by itself10:12 Sabbir holds 2C, then paysbooked: nobody else can touch it
Play চাপো। Nadia 10:01 এ 2C hold করে, কিন্তু টাকা দেয় না। 10:11 এ seat টা নিজে থেকেই free হয়ে যায়। Sabbir 10:12 এ সেটা hold করে টাকা দেয়, তাই শেষে seat টা booked।

কীভাবে পড়বে: কেউ hold করলে seat free → held হয়, টাকা দিলে held → booked। ফেরার একটাই রাস্তা, held → free: সময় শেষ, বা passenger ছেড়ে দিয়েছে। এই design এ booked seat আর ফেরত যায় না (booking cancel এখানে নেই)।

একটা seat, দুইজন ক্রেতা

Rahimhold 2ANadiahold 2AFlight.muone at a timeseat 2Afree → held (H1)RNrefusedboth press "hold 2A" in the same instantRahim gets the lock first: 2A is free, so it's held for himNadia gets the lock next: 2A is held, so she's refusedcheck and change happen under one lock: only one can win
Play চাপো। Rahim আর Nadia একই মুহূর্তে "hold 2A" চাপে। Rahim আগে lock পায়, 2A free দেখে hold করে নেয়। Nadia পরে lock পায়, 2A held দেখে, তাই তাকে না বলা হয়।

কীভাবে পড়বে: 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 (দামের জন্য)

  1. কেন: ভাড়া সবসময় বদলায়: class দিয়ে, flight কতটা ভরা তা দিয়ে, season দিয়ে, promo code দিয়ে।
  2. কী সমাধান করে: দাম ঠিক করা একটাই বদলানো যায় এমন object, Fare।
  3. না থাকলে: Hold এর ভেতরে if eid { … } else if lastSeats { … } বাড়তেই থাকে।
  4. Trade-off: একটা interface; খরচ প্রায় নেই।
  5. কোথায়: 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()

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 করে
TestHoldIsAllOrNothing2B নেওয়া থাকলে 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 gatewayPayment এ কয়েক সেকেন্ড লাগে; lock ধরে রেখে gateway ডেকো না। Lock এর ভেতরে hold টাকে "paying" mark করো, lock ছেড়ে gateway ডাকো, তারপর আবার lock নিয়ে booking করো (payment fail হলে seat ছেড়ে দাও)। Payment চলার সময় deadline বাড়িয়ে দাও।
অনেকগুলো serverMemory র 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 cancelRefund এর rule সহ booked → free যোগ করো; এটা একটা নতুন transition, আর সম্ভবত একটা Refund strategy।
Overbooking (airline রা আসলেই করে)Economy তে capacity + N পর্যন্ত hold করতে দাও; এটা Hold এর ভেতরের একটা business rule হয়, seat এর state বদলায় না।

Interview প্রশ্ন

Practice প্রশ্ন

Beginner

  1. একটা seat কোন কোন state এ থাকতে পারে, আর কী হলে এক state থেকে আরেকটায় যায়?
  2. Click করলেই book না করে আগে hold কেন?

Intermediate

  1. একটা পরিবারের তিনটা seat হয় একসাথে hold হবে নয় একটাও না, এটা কীভাবে করবে?
  2. ১০ মিনিটের timeout ১০ মিনিট অপেক্ষা না করে কীভাবে test করবে?

Advanced

  1. দুইজন একই millisecond এ "hold 2A" চাপল। Lock সহ আর lock ছাড়া কী হয়, ধাপে ধাপে বলো।
  2. 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 ট্র্যাক হবে।