সব problem (16)
System Design courseSystem Design Problems

0%

0/16 done

Practice · Low Level Design · Model the Domain

Design a Parking Lot

BeginnerEntitiesEnumsStrategyPricing⏱ 1-2 ঘণ্টা

সবচেয়ে বেশি আসা LLD warm-up: ভিন্ন size এর spot, ticket, বদলানো যায় এমন pricing strategy, আর code সহ পুরো class design।

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

Problem এর পরিচয়

DifficultyBeginner
সময়Design আর code মিলিয়ে ১–২ ঘণ্টা; interview তে মোটামুটি ৪৫ মিনিট
আগে জানা দরকারClass আর object, interface, enum, lock সম্পর্কে একটু ধারণা
যা শিখবেEntity খুঁজে বের করা, encapsulation, composition, Strategy, clock inject করা, shared state এর চারপাশে mutex
বাস্তবে কোথায়Shopping mall আর hospital এর parking, airport এর car park, gate এ "১২টা spot খালি" দেখানো app

LLD interview তে সবচেয়ে বেশি আসা warm-up হলো parking lot, আর এর কারণ আছে। Parking lot কী জিনিস সেটা সবাই চেনে, তাই domain বোঝানো নিয়ে সময় যায় না। আসল প্রশ্ন হলো, একটা ঘোলা গল্প থেকে তুমি পরিষ্কার দায়িত্বওয়ালা কয়েকটা class বের করতে পারো কিনা। আর interviewer যখন জিজ্ঞেস করবে "pricing বদলাতে চাইলে?", তোমার design টা তখন টিকে থাকে কিনা।

Problem statement

ঢাকার কোনো shopping mall এর নিচের car park টার কথা ভাবো। কয়েকটা floor আছে। প্রতিটা floor এ ভিন্ন size এর spot: বাইকের জন্য চাপা জায়গা, গাড়ির জন্য সাধারণ, pickup আর ছোট truck এর জন্য চওড়া।

একটা গাড়ি gate এ আসলো। System কে তার জন্য একটা spot খুঁজে দিতে হবে, driver কে ticket দিতে হবে। পরে গাড়ি বের হওয়ার সময় যতক্ষণ ছিল সেই হিসাবে টাকা নিতে হবে, আর spot টা পরের গাড়ির জন্য খালি করে দিতে হবে।

আমরা কী বানাচ্ছি: gate machine এর ভেতরের software টা। Barrier এর motor না, camera না, শুধু সেই অংশ যেটা ঠিক করে কে কোথায় park করবে আর কত টাকা দেবে।

এটা কেন দরকার: এটা না থাকলে guard রা ঘুরে ঘুরে খালি spot খোঁজে, driver রা floor এর পর floor চক্কর দেয়, আর exit এ কত টাকা নেওয়া হবে সেটা নির্ভর করে counter এ কে বসে আছে তার উপর।

এক লাইনে কাজটা: একটা vehicle আসলে তাকে সবচেয়ে ভালো খালি spot আর একটা ticket দাও। একটা ticket আসলে ঠিক fee নাও আর spot খালি করো।

Requirement

Functional requirement

  1. Lot এ এক বা একাধিক level আছে। প্রতিটা level এ তিন size এর spot: small, medium আর large।
  2. তিন রকম vehicle: motorcycle (small লাগে), car (medium লাগে), truck (large লাগে)। দরকারের চেয়ে বড় spot এ রাখা যাবে, ছোটতে কখনো না।
  3. Park: vehicle এর জন্য একটা খালি spot খুঁজে ticket দাও, যেখানে থাকবে ID, spot আর ঢোকার সময়। আগে সবচেয়ে নিচের level দেখবে, আর সেই level এ যে spot এ আঁটে তার মধ্যে সবচেয়ে ছোটটা।
  4. Leave: ticket নিয়ে fee হিসাব করো, ticket টা paid করো, আর spot খালি করো।
  5. Pricing: প্রতিটা শুরু হওয়া ঘণ্টার জন্য টাকা, vehicle অনুযায়ী আলাদা rate, কমপক্ষে এক ঘণ্টা। Lot এর মালিক pricing এর নিয়ম বদলাতে পারবে (যেমন খেলার দিন এক দাম), কিন্তু parking এর code না ছুঁয়ে।
  6. খালি spot এর হিসাব: কোন size এর কয়টা spot খালি সেটা দেখাও, gate এর board এর জন্য।
  7. ভুল input ফেরত দাও: plate ছাড়া vehicle, যে plate আগে থেকেই ভেতরে, অচেনা ticket, আর যে ticket এর টাকা আগেই দেওয়া হয়ে গেছে।

Non-functional requirement

Requirementএখানে এর মানে
Concurrency তেও ঠিক থাকাএকটা lot এ কয়েকটা gate থাকে। একই মুহূর্তে আসা দুইটা গাড়ি কখনো একই spot পাবে না।
Extensibilityনতুন pricing, নতুন vehicle type, নতুন spot এর নিয়ম, এগুলো যোগ করতে হবে, পুরনো code আবার লিখতে হবে না।
TestabilityFee সময়ের উপর নির্ভর করে। Test এ "আড়াই ঘণ্টা পরে" check করতে হবে, sleep না করেই।
Performanceএকটা mall এর lot এ কয়েকশো spot। প্রতিটা গাড়ির জন্য একবার পুরো list ঘুরে দেখলেও সমস্যা নেই। কখন এটা আর চলবে না সেটাও দেখাবো।
Maintainabilityপ্রতিটা class এর কাজ এক বাক্যে বলা যায়।

যা ধরে নিচ্ছি আর যা এখানে নেই

  • একটা building, সব memory তে। Database নেই, network নেই। ওটা এই problem এর HLD version।
  • Payment সবসময় সফল হয়। Card, bKash আর cash সামলানো আলাদা একটা পুরো design এর ব্যাপার।
  • Reservation, EV charging spot আর ticket হারানোর flow নেই। Design review তে এগুলো "যদি…" প্রশ্ন হিসেবে আবার আসবে।

যা জমা দিতে হবে

  • Entity এর list, প্রতিটার কাজ এক বাক্যে।
  • একটা class diagram।
  • Park আর Leave এর code, যেখানে pricing এর নিয়ম বদলানো যায়।
  • Fee এর নিয়ম আর শেষ spot এর জন্য দুই gate এর দৌড়, এই দুইটার test।
Hint 1: noun গুলো কোথায় নিয়ে যায়?

Requirement এর noun গুলোর নিচে দাগ দাও: lot, level, spot, vehicle, ticket, fee। বেশিরভাগই class হবে। শুধু "fee" টা আলাদা: এটা কোনো জিনিস না, এটা একটা নিয়ম। যে নিয়ম বদলায়, সেটা interface এর দিকে ইশারা করে।

Hint 2: গাড়ি কোথায় যাবে, কে ঠিক করবে?

Spot না, vehicle ও না। Spot ঠিক করতে গেলে প্রতিটা spot কে বাকি সব spot এর খবর রাখতে হতো। সিদ্ধান্তটা রাখো সেই object এ, যে সব level একসাথে দেখতে পায়।

Hint 3: "যেটায় আঁটে তার মধ্যে সবচেয়ে ছোট"

প্রতিটা level যদি তার spot গুলো ছোট থেকে বড় ক্রমে রাখে, তাহলে প্রথম যে খালি spot টা যথেষ্ট বড়, সেটাই best fit। Park করার সময় আর sort করতে হয় না।

Hint 4: সময় test করা

Lot এর ভেতরে গিয়ে সরাসরি time.Now() ডেকো না। Lot কে এমন একটা function দাও যেটা সময় বলে দেয়, আর test এ একটা নকল function পাঠাও।

মূল entity

Requirement পড়ে noun গুলো তুলে আনো। তারপর প্রতিটাকে জিজ্ঞেস করো: এর কি নিজের state আছে, কোনো behaviour আছে, আর এটা না থাকলে কি design খারাপ হতো?

Entityএক বাক্যে কাজকী রাখেকী করে
VehicleTypeকী ধরনের vehicle সেটা বলেএকটা enum valueকোন size এর spot লাগবে সেটা বলে দেয়
VehicleGate এ যে দাঁড়িয়ে আছেplate, typeকিছু না, শুধু একটা value
SpotSizeক্রম আছে এমন size, যাতে "বড়টায় ছোটটা আঁটে" এক comparison এ হয়ে যায়একটা enum value>= দিয়ে তুলনা
SpotFloor এ রং করা একটা চতুর্ভুজID, level, size, কে আছেখালি কিনা জানে
Levelএকটা floorতার spot গুলো, ছোট থেকে বড়যেটায় আঁটে এমন সবচেয়ে ভালো খালি spot খোঁজে
TicketDriver এর রসিদID, plate, spot, ঢোকা, বের হওয়া, fee, statusকিছু না, lot যে data ভরে দেয়
PricingStrategyযে নিয়মে থাকার সময় টাকায় বদলায়নিয়ম অনুযায়ীFee(vehicle, stay)
Lotএকমাত্র প্রবেশপথ, আর একমাত্র object যে সব দেখেlevel, ticket, কে কোথায় আছে, clock, lockPark, Leave, Free

দুইটা জিনিস ইচ্ছা করেই class না:

  • Gate. এই scope এ gate এর কাজ শুধু Lot.Park আর Lot.Leave ডাকা। যে class শুধু call টা আরেকজনের কাছে পাঠিয়ে দেয়, সে একটা layer যোগ করে কিন্তু কোনো সিদ্ধান্ত যোগ করে না। পরে gate এর নিজের কাজ আসলে (plate পড়ার camera, যে printer আটকে যেতে পারে), তখন সে class হওয়ার যোগ্য।
  • Payment. এটা scope এর বাইরে, আর আনলে পুরো আরেকটা design টেনে আনতে হবে।

কে কার সাথে কীভাবে যুক্ত:

  • Lot তার Level গুলোর মালিক, আর প্রতিটা Level তার Spot গুলোর মালিক। এটা composition: level ছাড়া spot এর কোনো অস্তিত্ব নেই, lot ছাড়া level এর ও না।
  • Lot Ticket বানায় আর আসলটা নিজের কাছে রাখে। Driver সবসময় একটা copy পায়।
  • Lot একটা interface দিয়ে PricingStrategy ব্যবহার করে। পেছনে কোন নিয়ম আছে সেটা সে জানে না, জানার দরকারও নেই।
  • Vehicle বাইরে থেকে আসে, object হিসেবে কোথাও রাখা হয় না। Lot শুধু plate আর type রাখে, ওটুকুই তার লাগে।

Diagram

Class diagram

Vehicle+ Plate: string+ Type: VehicleTypeLot- levels: []*Level- tickets: map[string]*Ticket- parked: map[string]*Ticket- pricing: PricingStrategy- mu: sync.Mutex+ Park(v Vehicle) (Ticket, error)+ Leave(ticketID) (Ticket, error)+ Free() map[SpotSize]intTicket+ ID, Plate, SpotID: string+ Entry, Exit: time.Time+ Fee: int+ Status: TicketStatusLevel+ Number: int- spots: []*Spot- findSpot(need) *SpotSpot+ ID: string+ Size: SpotSize- plate: string- free() bool«interface»PricingStrategy+ Fee(v, stay) intHourlyPricing+ Rates: map[VehicleType]int+ Fee(v, stay) intFlatPricing+ Amount: int+ Fee(v, stay) intparksticketslevels1*spots1*pricingLot owns its levels, and each level owns its spots (◆ = owns)Park takes a Vehicle; Lot keeps every Ticket it hands outLot only knows the PricingStrategy interfaceHourlyPricing and FlatPricing implement it (dashed ▷)
Play চাপো। আগে Lot এর নিজের level আর spot গুলো জ্বলে, তারপর সে যে Vehicle park করে আর যে Ticket গুলো রাখে, তারপর যে pricing interface এর উপর সে নির্ভর করে, শেষে সেটা implement করা দুইটা strategy।

কীভাবে পড়বে: প্রতিটা box একটা class: উপরে নাম, তারপর field, তারপর method। বাম দিকে মালিকানা: একটা Lot এর অনেক Level, প্রতিটা Level এর অনেক Spot। Lot যত Ticket দেয়, সবগুলো নিজের কাছেও রাখে। ডান দিকে Lot এর কাছে একটা PricingStrategy, যেটা একটা interface; HourlyPricing আর FlatPricing তার দুইটা উত্তর। এই একটা তীরের জন্যই খেলার দিন Lot এ হাত না দিয়ে flat ভাড়ায় চলে যাওয়া যায়।

চিহ্ন গুলো: + মানে public, - মানে private। ভরা diamond ◆ মানে মালিক (অংশটা মালিকের সাথেই আসে, সাথেই যায়), সাধারণ তীর মানে reference ধরে রাখে, dashed তীর মানে ব্যবহার করে, আর ফাঁপা ত্রিভুজওয়ালা dashed তীর মানে এই interface implement করে।

Sequence: একটা গাড়ি park করছে

CAR-CLotLevel 1Level 2Park(CAR-C, car)lock, check platefindSpot(medium)nil: nothing big enoughfindSpot(medium)spot S1mark S1, ticket T0005Ticket T0005 (L2-S1)
CAR-C আসলো। Level 1 এ আর আঁটার মতো কিছু নেই, Level 2 তে আছে, তাই ticket এ লেখা L2-S1।

কীভাবে পড়বে: প্রতিটা খাড়া dashed লাইন একটা object, আর সময় চলে উপর থেকে নিচে। ভরা তীর মানে call, dashed তীর মানে উত্তর ফেরত আসা, আর সরু bar গুলো দেখায় কোন object কখন কাজে ব্যস্ত। CAR-C Lot এর Park ডাকে। Lot একে একে প্রতিটা level কে জিজ্ঞেস করে। Level 1 বলে "nil": তার medium spot গুলো ভরা, large টাও। Level 2 তে S1 খালি। Lot spot টা দখল দেখায়, ticket T0005 বানায় আর একটা copy ফেরত দেয়।

Fail করলে কী হয়: সব level nil বললে lot ErrLotFull ফেরত দেয়, কোনো ticket তৈরি হয় না। কিছুই অর্ধেক হয়ে থাকে না, কারণ spot পাওয়ার পরেই শুধু সেটাকে দখল দেখানো হয়।

State: একটা ticket এর জীবন

Park()Activespot is takenPaidfee set, spot freeT0002Leave(): 150 takaLeave() againErrTicketPaid: no going back
Ticket শুরু হয় Active হিসেবে, Leave এ Paid হয়, আর একই ticket এ দ্বিতীয়বার Leave করলে ফিরিয়ে দেওয়া হয়।

কীভাবে পড়বে: ticket এর state মাত্র দুইটা। Park এটাকে Active হিসেবে বানায় (spot দখল)। Leave এটাকে Paid করে, fee বসায় আর spot খালি করে। ফেরার কোনো তীর নেই। Paid ticket এ আবার Leave করলে আসে ErrTicketPaid। এটাই আটকায় যাতে একটা buggy exit gate কারো spot দুইবার খালি করে না দেয়।

Concurrency: দুইটা gate, একটা spot

Gate ACAR-XGate BCAR-YLot.muone lockL1-S1the last spotCAR-XCAR-Ywaits for the locklot fullwithout the lock, both gates could see L1-S1 as free
Gate A আর Gate B দুজনেই শেষ spot টা চায়। Lock একবারে একজনকে ঢুকতে দেয়, তাই B L1-S1 ভাগ না পেয়ে দেখে lot ভরা।

কীভাবে পড়বে: শেষ খালি spot এর জন্য দুইটা gate একই মুহূর্তে Park ডাকে। Gate A আগে lock পায়, L1-S1 নেয়, lock ছেড়ে দেয়। Gate B এতক্ষণ অপেক্ষা করছিল। সে যখন ঢোকে, তখন আর কোনো spot নেই, তাই পায় "lot full"। Lock না থাকলে দুজনেই "L1-S1 খালি" পড়ে ফেলতে পারতো কেউ "দখল" লেখার আগেই, আর দুইটা গাড়ি পাঠানো হতো একটা চতুর্ভুজে। Testing section এর test টা দেখায় এটা সত্যিই ঘটে।

Requirement বাড়লে: একটা lock থাকায় পুরো Park call এর জন্য gate গুলো পালা করে ঢোকে। কয়েকশো spot এ এটা microsecond এর ব্যাপার, কেউ টেরও পায় না। বিশাল lot হলে পুরো lot এর জায়গায় প্রতিটা level এ আলাদা lock দেবে। Review section এ এটা আছে।

Design principle

শুধু সেই principle গুলো, যেগুলো সত্যিই এই design কে আকার দিয়েছে:

  • Single responsibility. Spot জানে সে খালি কিনা। Level একটা floor এ spot খোঁজে। Lot সবকিছু সমন্বয় করে। PricingStrategy দাম বলে। Pricing এর নিয়ম বদলালে ঠিক একটা type বদলায়।
  • Encapsulation. Spot এর plate field টা private (Go তে ছোট হাতের অক্ষর)। Package এর বাইরের কেউ Lot.Park ছাড়া spot দখল দেখাতে পারে না, তাই lock এড়িয়ে যাওয়ার রাস্তা নেই। Park আর Leave ticket এর copy ফেরত দেয়। কেউ নিজের copy তে Status = Paid লিখলে আসলটায় কিছুই বদলায় না।
  • Open/closed. Lot পরিবর্তনের জন্য বন্ধ, কিন্তু নতুন pricing এর জন্য খোলা: Fee method ওয়ালা একটা type লিখে পাঠিয়ে দিলেই হলো।
  • Dependency inversion. Lot নির্ভর করে PricingStrategy interface আর একটা now func() time.Time এর উপর, কোনো নির্দিষ্ট pricing type বা system clock এর উপর না। এজন্যই এটা test করা যায়।
  • Composition over inheritance. SmallSpot, MediumSpot, LargeSpot subclass ওয়ালা কোনো Spot base class নেই। Size গুলোর পার্থক্য শুধু data তে, behaviour এ না, তাই একটা Size field ই যথেষ্ট। এখানে subclass বানালে তিনটা class বাড়তো, সিদ্ধান্ত বাড়তো শূন্যটা।
  • KISS. Gate নেই, Floor interface নেই, spot খোঁজার আলাদা strategy নেই। প্রতিটাই পরে নেওয়ার মতো যুক্তিসঙ্গত ধাপ, কিন্তু এখনকার requirement এ কোনোটাই লাগে না।

Design pattern

Strategy: pricing এর জন্য

  1. কেন দরকার: মালিক fee এর হিসাব বদলাতে চায় (ঘণ্টা ধরে, এক দামে, রাতের rate, ছুটির দিনের rate), parking এর logic না ছুঁয়ে।
  2. কী সমাধান করে: "কত টাকা" প্রশ্নটা Lot থেকে বের করে একটা method ওয়ালা interface এর পেছনে রাখে: Fee(vehicle, stay)।
  3. না থাকলে কী হতো: Leave এর ভেতরে if isMatchDay { ... } else if isNight { ... } এর chain বাড়তেই থাকতো। প্রতিটা নতুন নিয়মে Lot edit করতে হতো, মানে দাম বদলাতে গিয়ে parking ভেঙে ফেলার ঝুঁকি।
  4. দাম কী: একটা type বেশি, আর একটা ধাপ ঘুরে call। Pricing যদি সত্যিই চিরকাল একই থাকতো, একটা সাধারণ function ই চলতো। কিন্তু requirement বলছে "parking এর code না বদলে বদলাতে হবে", তাই interface টা নিজের খরচ তুলে নেয়।
  5. কোথায় আছে: pricing.go তে PricingStrategy, HourlyPricing আর FlatPricing। Lot.Leave ডাকে l.pricing.Fee(...)। TestSwapPricing test টা FlatPricing দিয়ে lot বানায়, আর কিছুই বদলাতে হয় না।

Functional options: clock এর জন্য

NewLot(levels, pricing, WithClock(fake)) হলো Go তে optional setting দেওয়ার চলতি উপায়, আট parameter ওয়ালা constructor ছাড়াই। Java তে এর জায়গায় builder বা overloaded constructor, Python এ default সহ একটা keyword argument। Pattern এর নামের চেয়ে মূল idea টা বেশি জরুরি: সময়ও একটা dependency, তাই সেটা inject করো।

যে pattern গুলো নেইনি, আর কেন

  • Lot এর জন্য Singleton. লোভ লাগে ("lot তো একটাই"), কিন্তু এতে test গুলো state ভাগাভাগি করে, আর দ্বিতীয় building আনা অসম্ভব হয়ে যায়। main এ একটা Lot বানিয়ে সেটাই পাঠাতে থাকো।
  • Vehicle এর জন্য Factory. Vehicle মানে দুইটা field। Vehicle{Plate: "A", Type: Car} এর চেয়ে পরিষ্কার আর হয় না।
  • Ticket এর জন্য State pattern. দুইটা state আর একটা transition। একটা enum আর একটা if দিয়েই কথাটা সোজাভাবে বলা যায়। State pattern এর আসল জায়গা হলো Vending Machine।

Go তে পুরো solution

Project structure

parking/
├── go.mod
├── vehicle.go      VehicleType, Vehicle
├── spot.go         SpotSize, Spot, sizeFor
├── level.go        Level and best-fit search
├── ticket.go       Ticket, TicketStatus
├── pricing.go      PricingStrategy, HourlyPricing, FlatPricing
├── lot.go          Lot: Park, Leave, Free, the lock and the errors
├── lot_test.go     tests, including the concurrency test
└── cmd/demo/
    └── main.go     a morning at the lot

ধাপ ১: vehicle আর size

শব্দভাণ্ডার দিয়ে শুরু। SpotSize একটা ক্রমওয়ালা enum, তাই "এই spot এ আঁটবে?" প্রশ্নটা শুধু spot.Size >= need।

// vehicle.go
package parking

// VehicleType is what drives in. It decides which spots fit and what the hour costs.
type VehicleType int

const (
	Motorcycle VehicleType = iota
	Car
	Truck
)

func (v VehicleType) String() string {
	return [...]string{"motorcycle", "car", "truck"}[v]
}

// Vehicle is a value: two vehicles with the same plate and type are the same vehicle.
type Vehicle struct {
	Plate string
	Type  VehicleType
}
// spot.go
package parking

// SpotSize is ordered: a bigger spot can always take a smaller vehicle.
type SpotSize int

const (
	Small SpotSize = iota
	Medium
	Large
)

func (s SpotSize) String() string {
	return [...]string{"small", "medium", "large"}[s]
}

// sizeFor is the smallest spot a vehicle type fits in.
func sizeFor(t VehicleType) SpotSize {
	switch t {
	case Motorcycle:
		return Small
	case Car:
		return Medium
	default:
		return Large
	}
}

// Spot knows only itself: its size and who is in it. The lot decides who goes where.
type Spot struct {
	ID    string
	Level int
	Size  SpotSize
	plate string // "" means free
}

func (s *Spot) free() bool { return s.plate == "" }

ধাপ ২: যে level সবচেয়ে ভালো spot খুঁজে দেয়

কৌশলটা constructor এ: spot গুলো যোগ হয় ছোট থেকে বড় ক্রমে। তাই প্রথম যে খালি spot যথেষ্ট বড়, সেটাই আঁটে এমন সবচেয়ে ছোট spot। Park এর সময় sort নেই, "best fit" এর আলাদা logic ও নেই।

// level.go
package parking

import "fmt"

// Level is one floor. Its spots are kept smallest first, so the first free spot that
// fits is also the best fit: a motorcycle never takes a truck's spot while a small one
// is free.
type Level struct {
	Number int
	spots  []*Spot
}

func NewLevel(number, small, medium, large int) *Level {
	l := &Level{Number: number}
	n := 0
	add := func(count int, size SpotSize) {
		for range count {
			n++
			l.spots = append(l.spots, &Spot{ID: fmt.Sprintf("L%d-S%d", number, n), Level: number, Size: size})
		}
	}
	add(small, Small)
	add(medium, Medium)
	add(large, Large)
	return l
}

func (l *Level) findSpot(need SpotSize) *Spot {
	for _, s := range l.spots {
		if s.free() && s.Size >= need {
			return s
		}
	}
	return nil
}

ধাপ ৩: ticket আর pricing এর নিয়ম

// ticket.go
package parking

import "time"

type TicketStatus int

const (
	Active TicketStatus = iota // car is inside
	Paid                       // car has left; the ticket is closed for good
)

// Ticket is the receipt the driver holds. Callers get copies, so nobody outside the lot
// can flip a ticket to Paid without going through Leave.
type Ticket struct {
	ID      string
	Plate   string
	Vehicle VehicleType
	SpotID  string
	Entry   time.Time
	Exit    time.Time
	Fee     int // taka
	Status  TicketStatus
}

HourlyPricing integer হিসাব দিয়ে উপরের দিকে round করে। ভাগের আগে time.Hour - 1 যোগ করলে "১ ঘণ্টা ১ মিনিট" হয়ে যায় ২, আর ঠিক ১ ঘণ্টা থাকে ১। Float দিয়েও হতো, কিন্তু integer এর হিসাব হঠাৎ 0.9999 দিয়ে চমকে দেয় না।

// pricing.go
package parking

import "time"

// PricingStrategy turns a stay into money. The lot doesn't know how; it just asks.
type PricingStrategy interface {
	Fee(v VehicleType, stay time.Duration) int
}

// HourlyPricing charges per started hour, at least one hour.
type HourlyPricing struct {
	Rates map[VehicleType]int // taka per hour
}

func (p HourlyPricing) Fee(v VehicleType, stay time.Duration) int {
	hours := int((stay + time.Hour - 1) / time.Hour) // round up
	if hours < 1 {
		hours = 1
	}
	return hours * p.Rates[v]
}

// FlatPricing charges one amount per visit, e.g. on a match day.
type FlatPricing struct {
	Amount int
}

func (p FlatPricing) Fee(VehicleType, time.Duration) int { return p.Amount }

ধাপ ৪: lot, যেখানে সব সিদ্ধান্ত হয়

যা কিছু বদলায় (কোন spot এ কোন plate, কোন কোন ticket আছে), সেগুলো ছোঁয়া হয় শুধু mu ধরে থাকা অবস্থায়। Park আগে plate check করে, তারপর ক্রম ধরে level গুলো দেখে। তাই "সবচেয়ে নিচের level আগে" নিয়মটা loop থেকেই এমনি এসে যায়।

// lot.go
package parking

import (
	"errors"
	"fmt"
	"sync"
	"time"
)

var (
	ErrInvalidVehicle = errors.New("vehicle needs a plate")
	ErrLotFull        = errors.New("no free spot for this vehicle")
	ErrAlreadyParked  = errors.New("vehicle is already parked")
	ErrUnknownTicket  = errors.New("unknown ticket")
	ErrTicketPaid     = errors.New("ticket already paid")
)

// Lot is the one entry point: Park, Leave, Free. One mutex guards every spot and ticket,
// so two gates can never hand out the same spot.
type Lot struct {
	mu      sync.Mutex
	levels  []*Level
	pricing PricingStrategy
	now     func() time.Time
	spots   map[string]*Spot   // spot ID → spot
	tickets map[string]*Ticket // ticket ID → ticket
	parked  map[string]*Ticket // plate → active ticket
	nextID  int
}

type Option func(*Lot)

// WithClock swaps the clock, so tests can say "two and a half hours later" instantly.
func WithClock(now func() time.Time) Option {
	return func(l *Lot) { l.now = now }
}

func NewLot(levels []*Level, pricing PricingStrategy, opts ...Option) *Lot {
	l := &Lot{
		levels:  levels,
		pricing: pricing,
		now:     time.Now,
		spots:   map[string]*Spot{},
		tickets: map[string]*Ticket{},
		parked:  map[string]*Ticket{},
	}
	for _, lv := range levels {
		for _, s := range lv.spots {
			l.spots[s.ID] = s
		}
	}
	for _, o := range opts {
		o(l)
	}
	return l
}

// Park finds the best spot on the lowest level that has one and issues a ticket.
func (l *Lot) Park(v Vehicle) (Ticket, error) {
	if v.Plate == "" {
		return Ticket{}, ErrInvalidVehicle
	}
	l.mu.Lock()
	defer l.mu.Unlock()

	if _, ok := l.parked[v.Plate]; ok {
		return Ticket{}, ErrAlreadyParked
	}
	need := sizeFor(v.Type)
	for _, lv := range l.levels {
		s := lv.findSpot(need)
		if s == nil {
			continue
		}
		s.plate = v.Plate
		l.nextID++
		t := &Ticket{
			ID:      fmt.Sprintf("T%04d", l.nextID),
			Plate:   v.Plate,
			Vehicle: v.Type,
			SpotID:  s.ID,
			Entry:   l.now(),
			Status:  Active,
		}
		l.tickets[t.ID] = t
		l.parked[v.Plate] = t
		return *t, nil
	}
	return Ticket{}, ErrLotFull
}

// Leave closes the ticket, charges for the stay and frees the spot.
func (l *Lot) Leave(ticketID string) (Ticket, error) {
	l.mu.Lock()
	defer l.mu.Unlock()

	t, ok := l.tickets[ticketID]
	if !ok {
		return Ticket{}, ErrUnknownTicket
	}
	if t.Status == Paid {
		return Ticket{}, ErrTicketPaid
	}
	t.Exit = l.now()
	t.Fee = l.pricing.Fee(t.Vehicle, t.Exit.Sub(t.Entry))
	t.Status = Paid
	l.spots[t.SpotID].plate = ""
	delete(l.parked, t.Plate)
	return *t, nil
}

// Free counts free spots by size, for the board at the gate.
func (l *Lot) Free() map[SpotSize]int {
	l.mu.Lock()
	defer l.mu.Unlock()
	free := map[SpotSize]int{}
	for _, s := range l.spots {
		if s.free() {
			free[s.Size]++
		}
	}
	return free
}

ধাপ ৫: চালিয়ে দেখো

Demo টা দুই level এ ৬টা spot নিয়ে একটা lot খোলে আর ৭টা vehicle ঢোকায়। Clock একটা variable, তাই "আড়াই ঘণ্টা পরে" মানে এক লাইন।

// cmd/demo/main.go
package main

import (
	"fmt"
	"time"

	"parking"
)

func main() {
	clock := time.Date(2026, 11, 7, 9, 0, 0, 0, time.UTC)
	lot := parking.NewLot(
		[]*parking.Level{
			parking.NewLevel(1, 1, 2, 1), // 1 small, 2 medium, 1 large
			parking.NewLevel(2, 0, 1, 1),
		},
		parking.HourlyPricing{Rates: map[parking.VehicleType]int{
			parking.Motorcycle: 20, parking.Car: 50, parking.Truck: 100,
		}},
		parking.WithClock(func() time.Time { return clock }),
	)

	arrivals := []parking.Vehicle{
		{Plate: "BIKE-1", Type: parking.Motorcycle},
		{Plate: "CAR-A", Type: parking.Car},
		{Plate: "CAR-B", Type: parking.Car},
		{Plate: "TRUCK-1", Type: parking.Truck},
		{Plate: "CAR-C", Type: parking.Car},
		{Plate: "CAR-D", Type: parking.Car},
		{Plate: "TRUCK-2", Type: parking.Truck},
	}
	tickets := map[string]string{}
	for _, v := range arrivals {
		t, err := lot.Park(v)
		if err != nil {
			fmt.Printf("%-8s %-10s -> %v\n", v.Plate, v.Type, err)
			continue
		}
		tickets[v.Plate] = t.ID
		fmt.Printf("%-8s %-10s -> %s (ticket %s)\n", v.Plate, v.Type, t.SpotID, t.ID)
	}

	clock = clock.Add(2*time.Hour + 30*time.Minute)
	t, _ := lot.Leave(tickets["CAR-A"])
	fmt.Printf("CAR-A left after %v, fee %d taka\n", t.Exit.Sub(t.Entry), t.Fee)

	if _, err := lot.Leave(tickets["CAR-A"]); err != nil {
		fmt.Println("CAR-A again:", err)
	}

	// A medium spot is free now, but a truck needs a large one, and CAR-D is in it.
	if _, err := lot.Park(parking.Vehicle{Plate: "TRUCK-2", Type: parking.Truck}); err != nil {
		fmt.Println("TRUCK-2 retry ->", err)
	}

	free := lot.Free()
	fmt.Printf("free: small=%d medium=%d large=%d\n", free[parking.Small], free[parking.Medium], free[parking.Large])
}
$ go run ./cmd/demo
BIKE-1   motorcycle -> L1-S1 (ticket T0001)
CAR-A    car        -> L1-S2 (ticket T0002)
CAR-B    car        -> L1-S3 (ticket T0003)
TRUCK-1  truck      -> L1-S4 (ticket T0004)
CAR-C    car        -> L2-S1 (ticket T0005)
CAR-D    car        -> L2-S2 (ticket T0006)
TRUCK-2  truck      -> no free spot for this vehicle
CAR-A left after 2h30m0s, fee 150 taka
CAR-A again: ticket already paid
TRUCK-2 retry -> no free spot for this vehicle
free: small=0 medium=1 large=0

Output টা পড়ি:

  • BIKE-1 নেয় small spot L1-S1, যেটা ওর জন্যই বানানো।
  • CAR-A আর CAR-B Level 1 এর medium spot দুইটা ভরে ফেলে। TRUCK-1 নেয় large টা।
  • CAR-C যায় Level 2 তে, কারণ Level 1 এ গাড়ির জন্য আর কিছু খালি নেই।
  • CAR-D দেখে Level 2 এর medium spot ও দখল, তাই সে নেয় large spot L2-S2। এটা নিয়মের মধ্যেই: truck এর spot এ গাড়ি আঁটে।
  • TRUCK-2 কে ফিরিয়ে দেওয়া হয়: কোথাও আর large spot নেই।
  • CAR-A আড়াই ঘণ্টা পরে বের হয় আর ৩টা শুরু হওয়া ঘণ্টার টাকা দেয়: ৩ × ৫০ = ১৫০ টাকা। একই ticket এ দ্বিতীয়বার Leave ফিরিয়ে দেওয়া হয়।
  • TRUCK-2 আবার চেষ্টা করে। এখন একটা medium spot খালি, কিন্তু truck এর লাগে large, আর সেখানে বসে আছে CAR-D। এই লাইনটা মনে রাখো, design review তে এটাই এই problem এর সবচেয়ে ভালো "যদি…" প্রশ্ন।

বাকি পাঁচটা language

একই design, একই demo, একই output, শুধু প্রতিটা language তার নিজের ধরনে লেখা। নিচের প্রতিটা program চালানো হয়েছে, আর output Go এর সাথে লাইন ধরে মিলেছে।

from __future__ import annotations

import threading
from dataclasses import dataclass, replace
from datetime import datetime, timedelta
from enum import IntEnum
from typing import Callable, Protocol


class VehicleType(IntEnum):
    MOTORCYCLE = 0
    CAR = 1
    TRUCK = 2


class SpotSize(IntEnum):  # ordered: a bigger spot takes a smaller vehicle
    SMALL = 0
    MEDIUM = 1
    LARGE = 2


SIZE_FOR = {VehicleType.MOTORCYCLE: SpotSize.SMALL, VehicleType.CAR: SpotSize.MEDIUM,
            VehicleType.TRUCK: SpotSize.LARGE}


@dataclass(frozen=True)
class Vehicle:
    plate: str
    type: VehicleType


@dataclass
class Spot:
    id: str
    size: SpotSize
    plate: str = ""  # "" means free


@dataclass(frozen=True)  # frozen: callers can't flip a ticket to paid
class Ticket:
    id: str
    plate: str
    vehicle: VehicleType
    spot_id: str
    entry: datetime
    exit: datetime | None = None
    fee: int = 0
    paid: bool = False


class LotFull(Exception): ...
class AlreadyParked(Exception): ...
class UnknownTicket(Exception): ...
class TicketPaid(Exception): ...


class PricingStrategy(Protocol):
    def fee(self, v: VehicleType, stay: timedelta) -> int: ...


class HourlyPricing:
    def __init__(self, rates: dict[VehicleType, int]):
        self.rates = rates

    def fee(self, v: VehicleType, stay: timedelta) -> int:
        hours = max(1, -(-int(stay.total_seconds()) // 3600))  # round up, min 1
        return hours * self.rates[v]


class Level:
    def __init__(self, number: int, small: int, medium: int, large: int):
        sizes = [SpotSize.SMALL] * small + [SpotSize.MEDIUM] * medium + [SpotSize.LARGE] * large
        self.spots = [Spot(f"L{number}-S{i + 1}", s) for i, s in enumerate(sizes)]

    def find_spot(self, need: SpotSize) -> Spot | None:
        return next((s for s in self.spots if not s.plate and s.size >= need), None)


class Lot:
    def __init__(self, levels: list[Level], pricing: PricingStrategy,
                 now: Callable[[], datetime] = datetime.now):
        self._lock = threading.Lock()
        self._levels, self._pricing, self._now = levels, pricing, now
        self._spots = {s.id: s for lv in levels for s in lv.spots}
        self._tickets: dict[str, Ticket] = {}
        self._parked: dict[str, str] = {}  # plate -> ticket id
        self._next = 0

    def park(self, v: Vehicle) -> Ticket:
        if not v.plate:
            raise ValueError("vehicle needs a plate")
        with self._lock:
            if v.plate in self._parked:
                raise AlreadyParked("vehicle is already parked")
            for lv in self._levels:
                spot = lv.find_spot(SIZE_FOR[v.type])
                if spot:
                    spot.plate = v.plate
                    self._next += 1
                    t = Ticket(f"T{self._next:04d}", v.plate, v.type, spot.id, self._now())
                    self._tickets[t.id] = t
                    self._parked[v.plate] = t.id
                    return t
            raise LotFull("no free spot for this vehicle")

    def leave(self, ticket_id: str) -> Ticket:
        with self._lock:
            t = self._tickets.get(ticket_id)
            if t is None:
                raise UnknownTicket("unknown ticket")
            if t.paid:
                raise TicketPaid("ticket already paid")
            exit_at = self._now()
            t = replace(t, exit=exit_at, fee=self._pricing.fee(t.vehicle, exit_at - t.entry), paid=True)
            self._tickets[t.id] = t
            self._spots[t.spot_id].plate = ""
            del self._parked[t.plate]
            return t

    def free(self) -> dict[SpotSize, int]:
        with self._lock:
            out = {s: 0 for s in SpotSize}
            for s in self._spots.values():
                if not s.plate:
                    out[s.size] += 1
            return out


if __name__ == "__main__":
    clock = [datetime(2026, 11, 7, 9, 0)]
    lot = Lot([Level(1, 1, 2, 1), Level(2, 0, 1, 1)],
              HourlyPricing({VehicleType.MOTORCYCLE: 20, VehicleType.CAR: 50, VehicleType.TRUCK: 100}),
              now=lambda: clock[0])
    M, C, T = VehicleType.MOTORCYCLE, VehicleType.CAR, VehicleType.TRUCK
    tickets = {}
    for v in [Vehicle("BIKE-1", M), Vehicle("CAR-A", C), Vehicle("CAR-B", C), Vehicle("TRUCK-1", T),
              Vehicle("CAR-C", C), Vehicle("CAR-D", C), Vehicle("TRUCK-2", T)]:
        name = v.type.name.lower()
        try:
            t = lot.park(v)
            tickets[v.plate] = t.id
            print(f"{v.plate:<8} {name:<10} -> {t.spot_id} (ticket {t.id})")
        except LotFull as e:
            print(f"{v.plate:<8} {name:<10} -> {e}")

    clock[0] += timedelta(hours=2, minutes=30)
    t = lot.leave(tickets["CAR-A"])
    h, m = divmod(int((t.exit - t.entry).total_seconds()) // 60, 60)
    print(f"CAR-A left after {h}h{m}m0s, fee {t.fee} taka")
    try:
        lot.leave(tickets["CAR-A"])
    except TicketPaid as e:
        print("CAR-A again:", e)
    try:
        lot.park(Vehicle("TRUCK-2", T))
    except LotFull as e:
        print("TRUCK-2 retry ->", e)
    f = lot.free()
    print(f"free: small={f[SpotSize.SMALL]} medium={f[SpotSize.MEDIUM]} large={f[SpotSize.LARGE]}")

কীভাবে চালাবে: python3 parking.py, node parking.ts (Node 23+ সরাসরি TypeScript চালায়), rustc --edition 2021 parking.rs && ./parking, javac Parking.java && java Parking (Java 21), g++ -std=c++20 parking.cpp -o parking && ./parking।

এক language থেকে আরেকটায় কী বদলায়, আর কেন:

"Pricing বদলানো যায়" কীভাবে লেখাTicket কীভাবে নিরাপদLock কীভাবে কাজ করে
Gointerface, আলাদা করে ঘোষণা না করেই মেনে চলেstruct এর value ফেরত দেয়, তাই caller পায় copysync.Mutex + defer Unlock()
PythonProtocol: fee method থাকলেই চলবে@dataclass(frozen=True); paid copy বানায় replace()with সহ threading.Lock
TypeScriptinterfacereadonly field; Leave নতুন object ফেরত দেয়lock নেই: JS এই code এক thread এ চালায়, আর park এ কোনো await নেই, তাই মাঝপথে কেউ ঢুকতে পারে না
Rusttrait object Box<dyn PricingStrategy>, সাথে Send + Syncclone() ফেরত দেয়; lot এর ভেতরের কোনো reference কেউ পায় নাবদলায় এমন সব state থাকে Mutex এর ভেতরে, তাই lock ছাড়া spot ছুঁতে গেলে compiler ই আটকে দেয়
Javainterfacerecord immutable; Leave নতুন একটা বানায়synchronized method গুলো object এর একটাই lock ভাগ করে
C++pure virtual fee সহ abstract classvalue হিসেবে ফেরত দেয়std::lock_guard (RAII): প্রতিটা return আর throw তে নিজে থেকেই unlock

Rust এর লাইনটা আরেকবার দেখো। Go, Java আর C++ এ lock আর সে যে data পাহারা দেয় সেগুলো দুইটা আলাদা field, শুধু নিজের সাবধানতা দিয়ে এদের একসাথে রাখতে হয়। Rust এ data টাই mutex এর ভেতরে, তাই lock ভুলে গেলে সেটা compile error, production এ গিয়ে ধরা পড়া bug না।

Testing

// lot_test.go
package parking

import (
	"errors"
	"fmt"
	"sync"
	"testing"
	"time"
)

var rates = HourlyPricing{Rates: map[VehicleType]int{Motorcycle: 20, Car: 50, Truck: 100}}

// fakeClock is a clock the test moves by hand.
type fakeClock struct{ t time.Time }

func (c *fakeClock) now() time.Time          { return c.t }
func (c *fakeClock) add(d time.Duration)     { c.t = c.t.Add(d) }
func newClock() *fakeClock                   { return &fakeClock{time.Date(2026, 1, 1, 9, 0, 0, 0, time.UTC)} }
func newLot(c *fakeClock, lv ...*Level) *Lot { return NewLot(lv, rates, WithClock(c.now)) }

func TestBestFitSpot(t *testing.T) {
	lot := newLot(newClock(), NewLevel(1, 1, 1, 1))
	cases := []struct {
		v    Vehicle
		spot string
	}{
		{Vehicle{"B1", Motorcycle}, "L1-S1"}, // small, its own size
		{Vehicle{"B2", Motorcycle}, "L1-S2"}, // small is taken: medium is next best
		{Vehicle{"C1", Car}, "L1-S3"},        // only large is left, a car still fits
	}
	for _, c := range cases {
		tk, err := lot.Park(c.v)
		if err != nil || tk.SpotID != c.spot {
			t.Fatalf("%s: got %q, %v; want %s", c.v.Plate, tk.SpotID, err, c.spot)
		}
	}
}

func TestLowerLevelFirst(t *testing.T) {
	lot := newLot(newClock(), NewLevel(1, 0, 1, 0), NewLevel(2, 0, 1, 0))
	a, _ := lot.Park(Vehicle{"A", Car})
	b, _ := lot.Park(Vehicle{"B", Car})
	if a.SpotID != "L1-S1" || b.SpotID != "L2-S1" {
		t.Fatalf("got %s, %s", a.SpotID, b.SpotID)
	}
}

func TestTruckNeedsLarge(t *testing.T) {
	lot := newLot(newClock(), NewLevel(1, 3, 3, 0))
	if _, err := lot.Park(Vehicle{"T", Truck}); !errors.Is(err, ErrLotFull) {
		t.Fatalf("want ErrLotFull, got %v", err)
	}
}

func TestInvalidInput(t *testing.T) {
	lot := newLot(newClock(), NewLevel(1, 1, 1, 1))
	if _, err := lot.Park(Vehicle{"", Car}); !errors.Is(err, ErrInvalidVehicle) {
		t.Fatalf("empty plate: %v", err)
	}
	if _, err := lot.Leave("T9999"); !errors.Is(err, ErrUnknownTicket) {
		t.Fatalf("unknown ticket: %v", err)
	}
}

func TestSamePlateTwice(t *testing.T) {
	lot := newLot(newClock(), NewLevel(1, 0, 2, 0))
	lot.Park(Vehicle{"A", Car})
	if _, err := lot.Park(Vehicle{"A", Car}); !errors.Is(err, ErrAlreadyParked) {
		t.Fatalf("want ErrAlreadyParked, got %v", err)
	}
}

func TestFeeRoundsUpHours(t *testing.T) {
	cases := []struct {
		stay time.Duration
		fee  int
	}{
		{0, 50},                        // minimum one hour
		{time.Hour, 50},                // exactly one hour
		{time.Hour + time.Minute, 100}, // a started hour counts
		{2*time.Hour + 30*time.Minute, 150},
	}
	for _, c := range cases {
		clk := newClock()
		lot := newLot(clk, NewLevel(1, 0, 1, 0))
		tk, _ := lot.Park(Vehicle{"A", Car})
		clk.add(c.stay)
		out, err := lot.Leave(tk.ID)
		if err != nil || out.Fee != c.fee {
			t.Errorf("stay %v: fee %d, %v; want %d", c.stay, out.Fee, err, c.fee)
		}
	}
}

func TestLeaveFreesSpotOnce(t *testing.T) {
	lot := newLot(newClock(), NewLevel(1, 0, 1, 0))
	tk, _ := lot.Park(Vehicle{"A", Car})
	if _, err := lot.Leave(tk.ID); err != nil {
		t.Fatal(err)
	}
	if _, err := lot.Leave(tk.ID); !errors.Is(err, ErrTicketPaid) {
		t.Fatalf("second leave: %v", err)
	}
	if again, err := lot.Park(Vehicle{"B", Car}); err != nil || again.SpotID != "L1-S1" {
		t.Fatalf("spot not reused: %q, %v", again.SpotID, err)
	}
}

func TestReturnedTicketIsACopy(t *testing.T) {
	lot := newLot(newClock(), NewLevel(1, 0, 1, 0))
	tk, _ := lot.Park(Vehicle{"A", Car})
	tk.Status = Paid // the caller scribbles on its copy
	if _, err := lot.Leave(tk.ID); err != nil {
		t.Fatalf("lot's ticket changed from outside: %v", err)
	}
}

func TestSwapPricing(t *testing.T) {
	clk := newClock()
	lot := NewLot([]*Level{NewLevel(1, 0, 1, 0)}, FlatPricing{Amount: 200}, WithClock(clk.now))
	tk, _ := lot.Park(Vehicle{"A", Car})
	clk.add(5 * time.Hour)
	if out, _ := lot.Leave(tk.ID); out.Fee != 200 {
		t.Fatalf("flat fee: %d", out.Fee)
	}
}

// 100 cars race for 50 spots through many gates at once: exactly 50 get in, and no spot
// is handed out twice. Run with -race.
func TestConcurrentGates(t *testing.T) {
	lot := newLot(newClock(), NewLevel(1, 0, 25, 0), NewLevel(2, 0, 25, 0))
	var wg sync.WaitGroup
	var mu sync.Mutex
	taken := map[string]string{}
	full := 0
	for i := range 100 {
		wg.Add(1)
		go func() {
			defer wg.Done()
			tk, err := lot.Park(Vehicle{fmt.Sprintf("CAR-%d", i), Car})
			mu.Lock()
			defer mu.Unlock()
			if errors.Is(err, ErrLotFull) {
				full++
				return
			}
			if other, dup := taken[tk.SpotID]; dup {
				t.Errorf("%s given to %s and %s", tk.SpotID, other, tk.Plate)
			}
			taken[tk.SpotID] = tk.Plate
		}()
	}
	wg.Wait()
	if len(taken) != 50 || full != 50 {
		t.Fatalf("parked %d, turned away %d", len(taken), full)
	}
}
$ go test -race -v ./...
=== RUN   TestBestFitSpot
--- PASS: TestBestFitSpot (0.00s)
=== RUN   TestLowerLevelFirst
--- PASS: TestLowerLevelFirst (0.00s)
=== RUN   TestTruckNeedsLarge
--- PASS: TestTruckNeedsLarge (0.00s)
=== RUN   TestInvalidInput
--- PASS: TestInvalidInput (0.00s)
=== RUN   TestSamePlateTwice
--- PASS: TestSamePlateTwice (0.00s)
=== RUN   TestFeeRoundsUpHours
--- PASS: TestFeeRoundsUpHours (0.00s)
=== RUN   TestLeaveFreesSpotOnce
--- PASS: TestLeaveFreesSpotOnce (0.00s)
=== RUN   TestReturnedTicketIsACopy
--- PASS: TestReturnedTicketIsACopy (0.00s)
=== RUN   TestSwapPricing
--- PASS: TestSwapPricing (0.00s)
=== RUN   TestConcurrentGates
--- PASS: TestConcurrentGates (0.00s)
PASS
ok  	parking	1.012s
?   	parking/cmd/demo	[no test files]

কোন test কী প্রমাণ করে:

Testযা check করে
TestBestFitSpotআগে আঁটে এমন সবচেয়ে ছোট spot, সেটা দখল থাকলে তার পরের size
TestLowerLevelFirstLevel 2 এর আগে Level 1 ভরে
TestTruckNeedsLargeঅনেক small spot খালি থাকলেও vehicle কখনো ছোট spot পায় না
TestInvalidInputখালি plate আর অচেনা ticket ঠিক error দিয়ে ফেরত যায়
TestSamePlateTwiceএকটা plate দুইটা spot ধরে রাখতে পারে না
TestFeeRoundsUpHoursসীমানা গুলো: ০ মিনিট, ঠিক ১ ঘণ্টা, ১ ঘণ্টা ১ মিনিট, আড়াই ঘণ্টা
TestLeaveFreesSpotOnceLeave spot খালি করে, দ্বিতীয় Leave ফেরত যায়, আর spot টা আবার ব্যবহার হয়
TestReturnedTicketIsACopyফেরত পাওয়া ticket বদলালে lot এর ticket বদলায় না
TestSwapPricingLot না বদলেই অন্য strategy চলে
TestConcurrentGates১০০টা goroutine, ৫০টা spot: ঠিক ৫০টা park হয়, কোনো spot দুইজন পায় না

Concurrency test টা কি আসলেই কিছু ধরে? আমরা দেখে নিয়েছি: Park থেকে lock এর দুইটা লাইন মুছে আবার চালাও।

$ go test -race -run Concurrent .
WARNING: DATA RACE
...
FAIL	parking	0.015s

প্রতিবার race detector data race দেখালো (আমাদের run এ ৫ থেকে ৮টা), আর test fail করলো। Lock ফেরত দিলে pass করে। যে test fix থাকলেও pass করে আর না থাকলেও pass করে, সে কিছুই প্রমাণ করে না। তাই একবার এভাবে check করে নেওয়া উচিত।

Design review

ধরো একজন teammate review এর জন্য এটা পাঠিয়েছে:

যা ভালো

  • প্রতিটা type এর একটা কাজ, আর সিদ্ধান্ত হয় শুধু Lot এ।
  • Pricing এর নিয়ম বা clock বদলাতে Lot এ একটা লাইনও বদলাতে হয় না।
  • ভুল করে lock এড়ানোর উপায় নেই: spot এর state private, আর প্রতিটা public method lock নেয়।
  • Test গুলো millisecond এ চলে, আর টাকার সব কোণাকাঞ্চি আর race দুটোই ধরে।

কী ভাঙতে পারে, আর requirement বদলালে কী হয়

"যদি…"কী ভাঙেকী বদলাতে হবে
গাড়ি গুলো large spot দখল করতেই থাকে (demo তে CAR-D আর TRUCK-2 দেখো)Truck এর spot এ গাড়ি বসে থাকে, আর truck ফিরে যায়Spot বাছাইকে দ্বিতীয় একটা strategy বানাও: "truck এর জন্য শুধু ঠিক size", বা শেষ N টা large spot truck এর জন্য রেখে দাও। এই মুহূর্তেই একটা SpotFinder interface এর দরকার হয়, তার আগে না।
১০,০০০ spot আর ২০টা gateএকটা lock এর জন্য প্রতিটা gate কে পুরো scan শেষ হওয়া পর্যন্ত বসে থাকতে হয়প্রতিটা level এ প্রতিটা size এর জন্য একটা free list রাখো (খালি spot ID এর queue), আর level ধরে lock দাও। Park তখন O(spots) থেকে O(levels) হয়ে যায়।
Electric car charging spotSpotSize একটা সরল ক্রম; "EV" বড়ও না, ছোটও নাSize এর পাশে spot এর feature যোগ করো (একটা set: ev, handicapped), আর দুটো মিলিয়ে দেখো।
Ticket হারিয়ে গেছেLeave এ ticket ID লাগেPlate দিয়ে active ticket খোঁজো (parked[plate] আমরা আগেই রাখি), আর হারানো ticket এর fee একটা pricing rule হিসেবে নাও।
Reservationখালি একটা spot হয়তো কাউকে কথা দেওয়া আছেSpot এর তৃতীয় একটা state লাগবে, reserved, মেয়াদ সহ। তখন এটা ছোট একটা state machine।
কারেন্ট চলে গেলসব memory তে, সব ticket উধাওTicket আর spot এর state save করো। এটাই HLD version: একটা database, আর lot হয়ে যায় একটা service।

যা ইচ্ছা করে সহজ রাখা

  • Ticket ID একটা counter, এক process এ এটা ঠিক আছে। কয়েকটা server জুড়ে হলে UUID বা একটা কেন্দ্রীয় ID service লাগবে।
  • খালি spot গুনতে প্রতিবার সব spot দেখা হয়। কয়েক সেকেন্ড পর পর refresh হওয়া gate board এর জন্য এটুকুই যথেষ্ট।

Production এ যাওয়ার আগে যা বদলাবো: প্রথম লাইনের spot বাছাইয়ের নিয়ম (আসল car park এ এটা নিয়ে সত্যিই অভিযোগ আসে), আর প্রতিটা Park আর Leave ticket ID সহ log করা, যাতে fee নিয়ে ঝামেলা হলে খুঁজে বের করা যায়।

Interview প্রশ্ন

Practice প্রশ্ন

Beginner

  1. খালি spot খোঁজার দায়িত্ব Spot এর না কেন?
  2. Park pointer না দিয়ে Ticket এর value কেন ফেরত দেয়?
  3. "আঁটে এমন সবচেয়ে ছোট spot" নিয়মটা code কীভাবে নিশ্চিত করে?

Intermediate

  1. রাত ১০টা থেকে সকাল ৬টা পর্যন্ত কম দামের night rate কীভাবে যোগ করবে?
  2. শেষ spot এর জন্য দুইটা gate একসাথে Park ডাকলো। ধাপে ধাপে কী হয় বলো।
  3. আড়াই ঘণ্টা অপেক্ষা না করে কীভাবে test করবে যে আড়াই ঘণ্টায় ১৫০ টাকা হয়?

Advanced

  1. Lot বড় হয়ে ১০,০০০ spot আর ২০টা gate হলো। Bottleneck কোথায়, আর কীভাবে ঠিক করবে?
  2. গাড়ি large spot নিয়ে নিচ্ছে আর truck ফিরে যাচ্ছে। Design টা এমনভাবে বদলাও যাতে নিয়মটা config করা যায়।
  3. Lot Singleton হলে এই code কেন খারাপ হতো?

Interviewer রা যে follow-up পছন্দ করে

  • "একটা floor শুধু motorcycle এর জন্য করো।" · "শুধু size না, প্রতিটা level এ কয়টা খালি সেটা দেখাও।" · "Level অনুযায়ী fee বদলাও (ছাদে দাম কম)।"

Design এর সাধারণ ভুল

  • প্রতিটা spot size এর জন্য subclass (SmallSpot, LargeSpot), যেখানে পার্থক্য শুধু data তে।
  • Pricing Leave এর ভেতরেই লেখা, if এর chain দিয়ে। তখন প্রতিটা নিয়ম বদলাতে parking এর code edit করতে হয়।
  • সরাসরি system clock ডাকা, এতে fee এর test হয় ধীর হয়, নয়তো কখনো pass করে কখনো করে না।
  • Concurrency পুরো ভুলে যাওয়া, অথবা শুধু "খোঁজা" ধাপে lock দেওয়া, "দখল দেখানো" ধাপে না। তাতে race টা থেকেই যায়।
  • ভেতরের pointer ফেরত দেওয়া, তাতে caller lock ছাড়াই ticket বা spot বদলে ফেলতে পারে।
  • শুধু call পাঠিয়ে দেওয়া একটা Gate class, যে একটা layer যোগ করে কিন্তু কোনো সিদ্ধান্ত নেয় না।

উত্তর

১. খালি spot খোঁজার দায়িত্ব Spot এর না কেন?

খুঁজতে গেলে সব spot দেখতে হয় আর তুলনা করতে হয়। একটা spot শুধু নিজেকে চেনে। Spot খুঁজলে তাকে তার ভাইবোনদের reference রাখতে হতো, আর "best fit" এর নিয়ম অনেক object এ ছড়িয়ে যেতো। খোঁজার কাজ তার, যে পুরো floor (Level) আর পুরো lot (Lot) দেখতে পায়।

২. Park pointer না দিয়ে value কেন দেয়?

Pointer দিলে caller lot এর নিজের ticket টাই বদলে ফেলতে পারতো, যেমন Status = Paid, lock ছাড়া আর spot খালি না করেই। Value মানে copy: caller পড়তে পারে, ইচ্ছামতো লিখতেও পারে, কিন্তু lot এর আসলটা ঠিক থাকে। TestReturnedTicketIsACopy ঠিক এটাই check করে।

৩. সবচেয়ে ছোট spot এর নিয়ম কীভাবে নিশ্চিত হয়?

NewLevel spot গুলো ছোট থেকে বড় ক্রমে যোগ করে। findSpot প্রথম যে খালি spot এর Size >= need, সেটা ফেরত দেয়। ক্রমের কারণে প্রথম মিলটাই আঁটে এমন সবচেয়ে ছোট। নিশ্চয়তাটা থাকে constructor এর ক্রমে, তাই ওখানে একটা comment থাকা জরুরি।

৪. Night rate কীভাবে যোগ করবে?

Fee method ওয়ালা নতুন একটা type লেখো, যেটা থাকার সময়কে রাত আর দিনের ঘণ্টায় ভাগ করে আলাদা দাম ধরে। তারপর সেটা NewLot এ পাঠাও। Lot বদলায় না। তবে একটা ঝামেলা আছে: Fee শুধু কতক্ষণ ছিল সেটা পায়, অথচ night rate এর লাগে আসল সময়টা। তাই interface টা বদলে Fee(vehicle, entry, exit) করতে হবে। Interview তে এটা মুখে বললে বাড়তি নম্বর: বোঝা যায় তুমি খেয়াল করেছো interface এ একটা তথ্য কম আছে।

৫. দুইটা gate, একটা spot: কী হয়?

দুজনেই Park ডাকে। একজন আগে mu পায়, spot খুঁজে পায়, দখল দেখায়, ticket বানায় আর unlock করে। অন্যজন এতক্ষণ Lock() এ আটকে ছিল। এবার সে খোঁজে, কিছু পায় না, আর ErrLotFull ফেরত দেয়। মূল কথা হলো "খোঁজা" আর "দখল দেখানো" দুটোই হয় একই critical section এর ভেতরে। আলাদা আলাদা lock দিলে দুজনেই একই খালি spot পেয়ে যেতে পারতো।

৬. আড়াই ঘণ্টার থাকা এক নিমিষে কীভাবে test করবে?

Lot কখনো সরাসরি time.Now() ডাকে না। সে ডাকে l.now(), যেটা default ভাবে time.Now, কিন্তু WithClock দিয়ে বদলানো যায়। Test একটা নকল clock দেয়, park করে, নকল clock ১৫০ মিনিট এগিয়ে দেয়, তারপর Leave ডাকে। কোনো sleep নেই, আর প্রতিবার একই ফল।

৭. ১০,০০০ spot আর ২০টা gate: bottleneck কোথায়?

দুই জায়গায়। প্রতিটা গাড়ির জন্য scan হয় O(spots), আর পুরোটা হয় একটা lock এর ভেতরে, তাই gate গুলো লাইনে দাঁড়ায়। Scan ঠিক করো প্রতিটা level এ প্রতিটা size এর free list দিয়ে: খালি spot ID এর একটা queue, তাতে spot খোঁজা মানে queue থেকে একটা তুলে নেওয়া। ভিড় কমাও পুরো lot এর বদলে level ধরে lock দিয়ে। তখন একটা gate শুধু একই level এ কাজ করা gate গুলোকে আটকায়। খালি spot এর হিসাব হয়ে যায় কয়েকটা counter, যেগুলো Park আর Leave এর সময় update হয়।

৮. Spot এর নিয়ম config করার ব্যবস্থা

"কোন vehicle কোন spot এ" প্রশ্নটা দ্বিতীয় একটা interface এর পেছনে নাও, যেমন Find(levels, vehicle) *Spot সহ SpotFinder। এখনকার behaviour হয়ে যায় BestFitFinder। নতুন type হিসেবে যোগ করো ExactSizeForTrucks বা ReserveLargeSpots{Keep: 2}। এটা pricing এর মতোই Strategy এর চাল, শুধু পার্থক্য হলো এটা নেওয়া হচ্ছে যখন সত্যিকারের দরকার এসেছে, আগেভাগে না।

৯. Singleton lot কেন খারাপ?

সব test একটা global lot ভাগ করতো, তাই test গুলো আর একসাথে চালানো যেতো না, পরিষ্কার অবস্থা থেকে শুরুও করা যেতো না। দ্বিতীয় building আনতে আবার লিখতে হতো। আর dependency টা লুকিয়ে যায়: function গুলো *Lot হাতে না পেয়ে global টা টেনে নেয়, তাই signature দেখে বোঝা যায় না কে parking এর state ছোঁয়। main এ একটা Lot বানিয়ে পাঠিয়ে দিলে "একটাই" ব্যাপারটা থাকে, এই ঝামেলা গুলো ছাড়াই।

Interviewer রা যা দেখে: একটা করে কাজওয়ালা পরিষ্কার entity, interface এর পেছনে pricing এর নিয়ম, concurrency এর আসল উত্তর (lock কোথায় আর কেন), আর পুরো design না ভেঙে "যদি…" প্রশ্ন সামলানোর ক্ষমতা।

নিজে design করে solution টা বুঝেছো? Mark করো, তাহলে progress ট্র্যাক হবে।