Practice · Low Level Design · Model the Domain
Design a Parking Lot
The most-asked LLD warm-up: spots of different sizes, tickets, a pricing strategy you can swap, and the full class design with code.
Learn mode: the full solution is shown below.
Problem introduction
| Difficulty | Beginner |
| Time | 1–2 hours to design and code; about 45 minutes in an interview |
| Prerequisites | Classes and objects, interfaces, enums, a little about locks |
| Concepts | Finding entities, encapsulation, composition, Strategy, injecting a clock, a mutex around shared state |
| Real world | Shopping mall and hospital parking, airport car parks, apps that show "12 spots free" at the gate |
The parking lot is the most-asked LLD warm-up for a reason. Anyone can picture it, so the interview isn't about the domain. It's about whether you can turn a fuzzy story into a small set of classes with clear jobs, and whether your design survives the interviewer's next question: "what if we change the pricing?"
Problem statement
Think of the car park under a shopping mall in Dhaka. It has a few floors. Each floor has spots of different sizes: tight ones for motorbikes, normal ones for cars, wide ones for pickups and small trucks.
A car drives up to the gate. The system has to find it a spot, give the driver a ticket, and later, when the car leaves, charge for the time it stayed and free the spot for the next car.
What we are building: the software inside the gate machine. Not the barrier motor, not the camera: the part that decides who parks where and how much they pay.
Why it exists: without it, guards walk around looking for empty spots, drivers circle the floors, and the money at the exit depends on who is at the counter.
What it must do, in one line: given a vehicle, give it the best free spot and a ticket; given a ticket, charge the right fee and free the spot.
Requirements
Functional requirements
- The lot has one or more levels. Each level has spots of three sizes: small, medium and large.
- Three vehicle types: motorcycle (needs small), car (needs medium), truck (needs large). A vehicle can use a bigger spot than it needs, never a smaller one.
- Park: find a free spot for the vehicle and issue a ticket with an ID, the spot and the entry time. Prefer the lowest level, and on that level the smallest spot that fits.
- Leave: take a ticket, compute the fee, mark the ticket paid and free the spot.
- Pricing: per started hour, with a different rate per vehicle type, minimum one hour. The lot owner must be able to change the pricing rule (for example a flat fee on match days) without changing the parking code.
- Free count: show how many spots of each size are free, for the board at the gate.
- Reject bad input: a vehicle with no plate, a plate that is already inside, an unknown ticket, a ticket that was already paid.
Non-functional requirements
| Requirement | What it means here |
|---|---|
| Correctness under concurrency | A lot has several gates. Two cars arriving at the same moment must never get the same spot. |
| Extensibility | New pricing rules, new vehicle types and new spot rules should be additions, not rewrites. |
| Testability | Fees depend on time. Tests must check "two and a half hours later" without sleeping. |
| Performance | A mall lot has a few hundred spots, so a linear scan per arrival is fine. We note where it stops being fine. |
| Maintainability | Each class has one job you can say in a sentence. |
Assumptions and out of scope
- One building, in memory. No database, no network: that's the HLD version of this problem.
- Payment always succeeds. Card, bKash and cash handling is a whole design of its own.
- No reservations, no EV charging spots, no lost-ticket flow. We come back to these in the design review as "what if" questions.
What to hand in
- A list of entities with one sentence each about their job.
- A class diagram.
- Code for
ParkandLeave, with the pricing rule swappable. - Tests that cover the fee rules and two gates racing for the last spot.
Hint 1: where do the nouns lead?
Underline the nouns in the requirements: lot, level, spot, vehicle, ticket, fee. Most become classes. "Fee" is the odd one out: it isn't a thing, it's a rule. Rules that change are a hint for an interface.
Hint 2: who decides where a car goes?
Not the spot, and not the vehicle. If Spot decided, every spot would need to know about every other spot. Put the decision in the one object that can see all levels.
Hint 3: "the smallest spot that fits"
If each level keeps its spots sorted smallest first, the first free spot that is big enough is automatically the best fit. No sorting at park time.
Hint 4: testing time
Don't call time.Now() deep inside the lot. Let the lot take a function that returns the time, and pass a fake one in tests.
Core entities
Read the requirements and pull out the nouns. Then ask of each one: does it hold state, does it have behaviour, and would the design be worse without it?
| Entity | Job in one sentence | Holds | Does |
|---|---|---|---|
VehicleType | Says what kind of vehicle this is | an enum value | maps to the spot size it needs |
Vehicle | The thing at the gate | plate, type | nothing: it's a plain value |
SpotSize | An ordered size, so "bigger fits smaller" is one comparison | an enum value | >= comparison |
Spot | One painted rectangle on the floor | ID, level, size, who is in it | knows if it's free |
Level | One floor | its spots, smallest first | finds the best free spot that fits |
Ticket | The driver's receipt | ID, plate, spot, entry, exit, fee, status | nothing: data the lot fills in |
PricingStrategy | The rule that turns a stay into money | depends on the rule | Fee(vehicle, stay) |
Lot | The one entry point, and the only object that sees everything | levels, tickets, who is parked where, the clock, the lock | Park, Leave, Free |
Two things are deliberately not classes:
- Gate. In this scope a gate does nothing except call
Lot.ParkandLot.Leave. A class that only forwards calls adds a layer without adding a decision. If gates later get their own behaviour (a camera that reads plates, a printer that can jam), that's when it earns a class. - Payment. Out of scope, and it would drag in a whole other design.
Relationships:
Lotowns itsLevels, and eachLevelowns itsSpots. This is composition: a spot doesn't exist outside its level, and a level doesn't exist outside its lot.LotcreatesTickets and keeps the originals. Drivers only ever get copies.Lotuses aPricingStrategythrough an interface. It doesn't know or care which rule is behind it.Vehicleis passed in and never stored as an object. The lot keeps the plate and the type, which is all it needs.
Diagrams
Class diagram
How to read it: each box is a class: its name, then its fields, then its methods. The left side is ownership: one Lot owns many Levels, and each Level owns many Spots. Lot also keeps every Ticket it hands out. On the right, Lot holds a PricingStrategy, which is an interface; HourlyPricing and FlatPricing are two answers to it. That one arrow is why you can switch to flat pricing on a match day without editing Lot.
Notation: + is public, - is private. A filled diamond ◆ means owns (the part lives and dies with its owner), a plain arrow means holds a reference to, a dashed arrow means uses, and a dashed arrow with a hollow triangle means implements this interface.
Sequence: a car parks
How to read it: each vertical dashed line is one object, and time runs downwards. A solid arrow is a call, a dashed arrow is the answer coming back, and the thin bars show when an object is busy. CAR-C calls Park on the Lot. The lot asks each level in order. Level 1 answers "nil": its medium spots are taken, and its large one too. Level 2 has S1 free. The lot marks the spot, makes ticket T0005 and hands a copy back.
The failure path: if every level answers nil, the lot returns ErrLotFull and no ticket exists. Nothing is half-done, because the spot is only marked once one is found.
State: a ticket's life
How to read it: a ticket has only two states. Park creates it as Active (its spot is taken). Leave moves it to Paid, sets the fee and frees the spot. There is no arrow back. Trying Leave on a paid ticket returns ErrTicketPaid, which is what stops a buggy exit gate from freeing someone else's spot twice.
Concurrency: two gates, one spot
How to read it: both gates call Park at the same moment for the last free spot. Gate A gets the lock first, takes L1-S1, and releases the lock. Gate B was waiting; when it gets in, there is no spot left, so it gets "lot full". Without the lock, both could read "L1-S1 is free" before either wrote "taken", and two cars would be sent to one rectangle. The test in the Testing section shows this really happens.
As requirements grow: with one lock, gates take turns for the whole Park call. For a few hundred spots that takes microseconds and nobody notices. For a huge lot, you would lock per level instead. The review section covers this.
Design principles
Only the principles that actually shaped this design:
- Single responsibility.
Spotknows if it's free.Levelfinds a spot on one floor.Lotcoordinates.PricingStrategyprices. When the pricing rule changes, exactly one type changes. - Encapsulation. A spot's
platefield is private (lowercase in Go). Nothing outside the package can mark a spot taken without going throughLot.Park, so the lock can't be skipped.ParkandLeavereturn copies of the ticket, so a caller who setsStatus = Paidon its copy changes nothing. - Open/closed. The lot is closed for modification but open for new pricing: add a type with a
Feemethod and pass it in. - Dependency inversion.
Lotdepends on thePricingStrategyinterface and on anow func() time.Time, not on a concrete pricing type or on the system clock. That's what makes it testable. - Composition over inheritance. There is no
Spotbase class withSmallSpot,MediumSpotandLargeSpotsubclasses. Sizes differ only in data, not behaviour, so aSizefield is enough. Subclasses here would add three classes and zero decisions. - KISS. No
Gate, noFloorinterface, no spot-finder strategy. Each of those is a reasonable later step, and none is needed for the requirements we have.
Design patterns
Strategy: for pricing
- Why it's needed: the owner wants to change how fees are computed (hourly, flat, night rate, weekend rate) without touching the parking logic.
- What it solves: it moves "how much" out of
Lotand behind an interface with one method,Fee(vehicle, stay). - Without it:
Leavegrows anif isMatchDay { ... } else if isNight { ... }chain. Every new rule editsLot, which risks breaking parking to change pricing. - Trade-offs: one more type and one indirection. If pricing were truly fixed forever, a plain function would do. Here the requirement says "change without changing the parking code", so the interface pays for itself.
- Where it is:
pricing.godefinesPricingStrategy,HourlyPricingandFlatPricing.Lot.Leavecallsl.pricing.Fee(...). The testTestSwapPricingbuilds a lot withFlatPricingand nothing else changes.
Functional options: for the clock
NewLot(levels, pricing, WithClock(fake)) is Go's usual way to give optional settings without a constructor with eight parameters. In Java you'd use a builder or an overloaded constructor; in Python a keyword argument with a default. The pattern matters less than the idea: time is a dependency, so inject it.
Patterns we did not use, and why
- Singleton for the lot. Tempting ("there is only one lot"), but it makes tests share state and makes a second building impossible. Create one
Lotinmainand pass it around. - Factory for vehicles. A vehicle is two fields.
Vehicle{Plate: "A", Type: Car}is already as clear as it gets. - State pattern for tickets. Two states and one transition. An enum and one
ifsay it more plainly. The Vending Machine is where State earns its place.
Complete solution in Go
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
Step 1: vehicles and sizes
Start with the vocabulary. SpotSize is an ordered enum, so "does this spot fit?" is just 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 == "" }
Step 2: a level that finds the best spot
The trick is in the constructor: spots are added smallest first. So the first free spot that's big enough is also the smallest one that fits. No sorting at park time, and no separate "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
}
Step 3: the ticket and the pricing rule
// 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 rounds up with integer arithmetic: adding time.Hour - 1 before dividing turns "1 hour 1 minute" into 2 and leaves exactly 1 hour as 1. Floats would work too, but integer maths can't surprise you with 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 }
Step 4: the lot, where the decisions live
Everything that changes (which spot holds which plate, which tickets exist) is touched only while mu is held. Park checks the plate first, then walks the levels in order, so "lowest level first" falls out of the loop for free.
// 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
}
Step 5: run it
The demo opens a lot with 6 spots on two levels and sends 7 vehicles in. The clock is a variable, so "two and a half hours later" is one line.
// 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
Reading the output:
BIKE-1takes the small spotL1-S1, the only one built for it.CAR-AandCAR-Bfill Level 1's medium spots.TRUCK-1takes the large one.CAR-Cgoes to Level 2, because nothing on Level 1 fits a car any more.CAR-Dfinds Level 2's medium spot taken, so it takes the large spotL2-S2. That's allowed: a car fits in a truck's spot.TRUCK-2is turned away: no large spot is left anywhere.CAR-Aleaves after 2h30m and pays for 3 started hours: 3 × 50 = 150 taka. A secondLeaveon the same ticket is refused.TRUCK-2tries again. A medium spot is free now, but a truck needs a large one, andCAR-Dis in it. Look at that line in the design review: it's the best "what if" in this problem.
The other five languages
Same design, same demo, same output, written the way each language likes. Every program below was run, and its output matched the Go output line for line.
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]}")// Ordered sizes: a bigger spot takes a smaller vehicle.
const SIZES = ["small", "medium", "large"] as const;
type SpotSize = 0 | 1 | 2;
type VehicleType = "motorcycle" | "car" | "truck";
const SIZE_FOR: Record<VehicleType, SpotSize> = { motorcycle: 0, car: 1, truck: 2 };
interface Vehicle { readonly plate: string; readonly type: VehicleType }
interface Spot { readonly id: string; readonly size: SpotSize; plate: string } // "" = free
// Readonly: callers get a ticket they can't mark as paid themselves.
interface Ticket {
readonly id: string; readonly plate: string; readonly vehicle: VehicleType;
readonly spotId: string; readonly entry: Date;
readonly exit?: Date; readonly fee: number; readonly paid: boolean;
}
interface PricingStrategy { fee(v: VehicleType, stayMs: number): number }
class HourlyPricing implements PricingStrategy {
private readonly rates: Record<VehicleType, number>;
constructor(rates: Record<VehicleType, number>) { this.rates = rates; }
fee(v: VehicleType, stayMs: number): number {
const hours = Math.max(1, Math.ceil(stayMs / 3_600_000));
return hours * this.rates[v];
}
}
class ParkingError extends Error {}
class Level {
readonly spots: Spot[] = [];
constructor(n: number, small: number, medium: number, large: number) {
const sizes: SpotSize[] = [...Array(small).fill(0), ...Array(medium).fill(1), ...Array(large).fill(2)];
sizes.forEach((size, i) => this.spots.push({ id: `L${n}-S${i + 1}`, size, plate: "" }));
}
findSpot(need: SpotSize): Spot | undefined {
return this.spots.find((s) => !s.plate && s.size >= need);
}
}
// No lock: JavaScript runs this code on one thread, and park() has no await inside,
// so two gates can't interleave in the middle of it.
class Lot {
private readonly spots = new Map<string, Spot>();
private readonly tickets = new Map<string, Ticket>();
private readonly parked = new Map<string, string>(); // plate -> ticket id
private next = 0;
private readonly levels: Level[];
private readonly pricing: PricingStrategy;
private readonly now: () => Date;
constructor(levels: Level[], pricing: PricingStrategy, now: () => Date = () => new Date()) {
this.levels = levels; this.pricing = pricing; this.now = now;
for (const lv of levels) for (const s of lv.spots) this.spots.set(s.id, s);
}
park(v: Vehicle): Ticket {
if (!v.plate) throw new ParkingError("vehicle needs a plate");
if (this.parked.has(v.plate)) throw new ParkingError("vehicle is already parked");
for (const lv of this.levels) {
const spot = lv.findSpot(SIZE_FOR[v.type]);
if (!spot) continue;
spot.plate = v.plate;
const t: Ticket = {
id: `T${String(++this.next).padStart(4, "0")}`, plate: v.plate, vehicle: v.type,
spotId: spot.id, entry: this.now(), fee: 0, paid: false,
};
this.tickets.set(t.id, t);
this.parked.set(v.plate, t.id);
return t;
}
throw new ParkingError("no free spot for this vehicle");
}
leave(ticketId: string): Ticket {
const t = this.tickets.get(ticketId);
if (!t) throw new ParkingError("unknown ticket");
if (t.paid) throw new ParkingError("ticket already paid");
const exit = this.now();
const done: Ticket = { ...t, exit, fee: this.pricing.fee(t.vehicle, exit.getTime() - t.entry.getTime()), paid: true };
this.tickets.set(t.id, done);
this.spots.get(t.spotId)!.plate = "";
this.parked.delete(t.plate);
return done;
}
free(): number[] {
const out = [0, 0, 0];
for (const s of this.spots.values()) if (!s.plate) out[s.size]++;
return out;
}
}
let clock = new Date(Date.UTC(2026, 10, 7, 9, 0));
const lot = new Lot(
[new Level(1, 1, 2, 1), new Level(2, 0, 1, 1)],
new HourlyPricing({ motorcycle: 20, car: 50, truck: 100 }),
() => clock,
);
const tickets = new Map<string, string>();
const arrivals: Vehicle[] = [
{ plate: "BIKE-1", type: "motorcycle" }, { plate: "CAR-A", type: "car" }, { plate: "CAR-B", type: "car" },
{ plate: "TRUCK-1", type: "truck" }, { plate: "CAR-C", type: "car" }, { plate: "CAR-D", type: "car" },
{ plate: "TRUCK-2", type: "truck" },
];
for (const v of arrivals) {
try {
const t = lot.park(v);
tickets.set(v.plate, t.id);
console.log(`${v.plate.padEnd(8)} ${v.type.padEnd(10)} -> ${t.spotId} (ticket ${t.id})`);
} catch (e) {
console.log(`${v.plate.padEnd(8)} ${v.type.padEnd(10)} -> ${(e as Error).message}`);
}
}
clock = new Date(clock.getTime() + 150 * 60_000);
const out = lot.leave(tickets.get("CAR-A")!);
const mins = (out.exit!.getTime() - out.entry.getTime()) / 60_000;
console.log(`CAR-A left after ${Math.floor(mins / 60)}h${mins % 60}m0s, fee ${out.fee} taka`);
try { lot.leave(tickets.get("CAR-A")!); } catch (e) { console.log("CAR-A again:", (e as Error).message); }
try { lot.park({ plate: "TRUCK-2", type: "truck" }); } catch (e) { console.log("TRUCK-2 retry ->", (e as Error).message); }
const f = lot.free();
console.log(`free: ${SIZES.map((s, i) => `${s}=${f[i]}`).join(" ")}`);use std::collections::HashMap;
use std::fmt;
use std::sync::Mutex;
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
enum VehicleType { Motorcycle, Car, Truck }
// Ordered: derive(PartialOrd) compares by declaration order, so Large > Small.
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
enum SpotSize { Small, Medium, Large }
impl VehicleType {
fn needs(self) -> SpotSize {
match self { Self::Motorcycle => SpotSize::Small, Self::Car => SpotSize::Medium, Self::Truck => SpotSize::Large }
}
}
impl fmt::Display for VehicleType {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let s = match self { Self::Motorcycle => "motorcycle", Self::Car => "car", Self::Truck => "truck" };
f.pad(s) // pad() honours {:<10}
}
}
struct Vehicle { plate: String, kind: VehicleType }
struct Spot { id: String, size: SpotSize, plate: Option<String> }
#[derive(Clone, Debug)] // callers get a clone, never a reference into the lot
struct Ticket { id: String, plate: String, vehicle: VehicleType, spot_id: String, entry: u64, exit: u64, fee: u32, paid: bool }
#[derive(Debug, PartialEq)]
enum ParkError { InvalidVehicle, LotFull, AlreadyParked, UnknownTicket, TicketPaid }
impl fmt::Display for ParkError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str(match self {
Self::InvalidVehicle => "vehicle needs a plate",
Self::LotFull => "no free spot for this vehicle",
Self::AlreadyParked => "vehicle is already parked",
Self::UnknownTicket => "unknown ticket",
Self::TicketPaid => "ticket already paid",
})
}
}
// Send + Sync so the lot can sit behind an Arc and be shared by gate threads.
trait PricingStrategy: Send + Sync { fn fee(&self, v: VehicleType, stay_secs: u64) -> u32; }
struct HourlyPricing { rates: HashMap<VehicleType, u32> }
impl PricingStrategy for HourlyPricing {
fn fee(&self, v: VehicleType, stay_secs: u64) -> u32 {
let hours = stay_secs.div_ceil(3600).max(1) as u32;
hours * self.rates[&v]
}
}
fn level(n: u32, small: usize, medium: usize, large: usize) -> Vec<Spot> {
let sizes = [(small, SpotSize::Small), (medium, SpotSize::Medium), (large, SpotSize::Large)];
sizes.iter().flat_map(|&(c, s)| std::iter::repeat_n(s, c)).enumerate()
.map(|(i, size)| Spot { id: format!("L{n}-S{}", i + 1), size, plate: None }).collect()
}
// Everything that changes lives inside the Mutex, so the compiler won't let you touch
// a spot without holding the lock.
struct State { levels: Vec<Vec<Spot>>, tickets: HashMap<String, Ticket>, parked: HashMap<String, String>, next: u32 }
struct Lot { state: Mutex<State>, pricing: Box<dyn PricingStrategy>, now: Box<dyn Fn() -> u64 + Send + Sync> }
impl Lot {
fn new(levels: Vec<Vec<Spot>>, pricing: Box<dyn PricingStrategy>, now: Box<dyn Fn() -> u64 + Send + Sync>) -> Self {
Lot { state: Mutex::new(State { levels, tickets: HashMap::new(), parked: HashMap::new(), next: 0 }), pricing, now }
}
fn park(&self, v: &Vehicle) -> Result<Ticket, ParkError> {
if v.plate.is_empty() { return Err(ParkError::InvalidVehicle); }
let mut st = self.state.lock().unwrap();
if st.parked.contains_key(&v.plate) { return Err(ParkError::AlreadyParked); }
let need = v.kind.needs();
let spot = st.levels.iter_mut().flatten()
.find(|s| s.plate.is_none() && s.size >= need)
.ok_or(ParkError::LotFull)?;
spot.plate = Some(v.plate.clone());
let spot_id = spot.id.clone();
st.next += 1;
let t = Ticket { id: format!("T{:04}", st.next), plate: v.plate.clone(), vehicle: v.kind, spot_id, entry: (self.now)(), exit: 0, fee: 0, paid: false };
st.tickets.insert(t.id.clone(), t.clone());
st.parked.insert(v.plate.clone(), t.id.clone());
Ok(t)
}
fn leave(&self, ticket_id: &str) -> Result<Ticket, ParkError> {
let mut st = self.state.lock().unwrap();
let now = (self.now)();
let t = st.tickets.get_mut(ticket_id).ok_or(ParkError::UnknownTicket)?;
if t.paid { return Err(ParkError::TicketPaid); }
t.exit = now;
t.fee = self.pricing.fee(t.vehicle, t.exit - t.entry);
t.paid = true;
let done = t.clone();
for s in st.levels.iter_mut().flatten() { if s.id == done.spot_id { s.plate = None; } }
st.parked.remove(&done.plate);
Ok(done)
}
fn free(&self) -> HashMap<SpotSize, usize> {
let st = self.state.lock().unwrap();
let mut out = HashMap::new();
for s in st.levels.iter().flatten().filter(|s| s.plate.is_none()) { *out.entry(s.size).or_insert(0) += 1; }
out
}
}
fn main() {
use std::sync::{atomic::{AtomicU64, Ordering}, Arc};
let clock = Arc::new(AtomicU64::new(0)); // seconds since 09:00
let c = clock.clone();
let rates = HashMap::from([(VehicleType::Motorcycle, 20), (VehicleType::Car, 50), (VehicleType::Truck, 100)]);
let lot = Lot::new(vec![level(1, 1, 2, 1), level(2, 0, 1, 1)], Box::new(HourlyPricing { rates }),
Box::new(move || c.load(Ordering::SeqCst)));
let v = |p: &str, k| Vehicle { plate: p.to_string(), kind: k };
use VehicleType::*;
let arrivals = [v("BIKE-1", Motorcycle), v("CAR-A", Car), v("CAR-B", Car), v("TRUCK-1", Truck),
v("CAR-C", Car), v("CAR-D", Car), v("TRUCK-2", Truck)];
let mut tickets = HashMap::new();
for a in &arrivals {
match lot.park(a) {
Ok(t) => { println!("{:<8} {:<10} -> {} (ticket {})", a.plate, a.kind, t.spot_id, t.id); tickets.insert(a.plate.clone(), t.id); }
Err(e) => println!("{:<8} {:<10} -> {}", a.plate, a.kind, e),
}
}
clock.store(2 * 3600 + 30 * 60, Ordering::SeqCst);
let t = lot.leave(&tickets["CAR-A"]).unwrap();
let mins = (t.exit - t.entry) / 60;
println!("CAR-A left after {}h{}m0s, fee {} taka", mins / 60, mins % 60, t.fee);
if let Err(e) = lot.leave(&tickets["CAR-A"]) { println!("CAR-A again: {e}"); }
if let Err(e) = lot.park(&v("TRUCK-2", Truck)) { println!("TRUCK-2 retry -> {e}"); }
let f = lot.free();
let n = |s| f.get(&s).copied().unwrap_or(0);
println!("free: small={} medium={} large={}", n(SpotSize::Small), n(SpotSize::Medium), n(SpotSize::Large));
}import java.time.Duration;
import java.time.Instant;
import java.util.*;
import java.util.function.Supplier;
public class Parking {
enum VehicleType { MOTORCYCLE, CAR, TRUCK }
// Ordered by declaration: compareTo says LARGE > SMALL.
enum SpotSize { SMALL, MEDIUM, LARGE;
static SpotSize forVehicle(VehicleType t) {
return switch (t) { case MOTORCYCLE -> SMALL; case CAR -> MEDIUM; case TRUCK -> LARGE; };
}
}
record Vehicle(String plate, VehicleType type) {}
// A record is immutable: Leave returns a new ticket instead of changing the old one.
record Ticket(String id, String plate, VehicleType vehicle, String spotId,
Instant entry, Instant exit, int fee, boolean paid) {}
static final class Spot {
final String id; final SpotSize size; String plate; // null = free
Spot(String id, SpotSize size) { this.id = id; this.size = size; }
}
static class ParkingException extends RuntimeException {
private static final long serialVersionUID = 1L;
ParkingException(String msg) { super(msg); }
}
interface PricingStrategy { int fee(VehicleType v, Duration stay); }
static final class HourlyPricing implements PricingStrategy {
private final Map<VehicleType, Integer> rates;
HourlyPricing(Map<VehicleType, Integer> rates) { this.rates = Map.copyOf(rates); }
public int fee(VehicleType v, Duration stay) {
long hours = Math.max(1, (stay.toSeconds() + 3599) / 3600);
return (int) hours * rates.get(v);
}
}
static final class Level {
final List<Spot> spots = new ArrayList<>();
Level(int n, int small, int medium, int large) {
int i = 0;
for (int k = 0; k < small; k++) spots.add(new Spot("L" + n + "-S" + ++i, SpotSize.SMALL));
for (int k = 0; k < medium; k++) spots.add(new Spot("L" + n + "-S" + ++i, SpotSize.MEDIUM));
for (int k = 0; k < large; k++) spots.add(new Spot("L" + n + "-S" + ++i, SpotSize.LARGE));
}
Optional<Spot> findSpot(SpotSize need) {
return spots.stream().filter(s -> s.plate == null && s.size.compareTo(need) >= 0).findFirst();
}
}
// synchronized methods share one lock: the object itself.
static final class Lot {
private final List<Level> levels;
private final PricingStrategy pricing;
private final Supplier<Instant> now;
private final Map<String, Spot> spots = new HashMap<>();
private final Map<String, Ticket> tickets = new HashMap<>();
private final Map<String, String> parked = new HashMap<>(); // plate -> ticket id
private int next = 0;
Lot(List<Level> levels, PricingStrategy pricing, Supplier<Instant> now) {
this.levels = List.copyOf(levels); this.pricing = pricing; this.now = now;
for (Level lv : levels) for (Spot s : lv.spots) spots.put(s.id, s);
}
synchronized Ticket park(Vehicle v) {
if (v.plate() == null || v.plate().isEmpty()) throw new IllegalArgumentException("vehicle needs a plate");
if (parked.containsKey(v.plate())) throw new ParkingException("vehicle is already parked");
SpotSize need = SpotSize.forVehicle(v.type());
for (Level lv : levels) {
Optional<Spot> found = lv.findSpot(need);
if (found.isEmpty()) continue;
Spot s = found.get();
s.plate = v.plate();
Ticket t = new Ticket(String.format("T%04d", ++next), v.plate(), v.type(), s.id, now.get(), null, 0, false);
tickets.put(t.id(), t);
parked.put(v.plate(), t.id());
return t;
}
throw new ParkingException("no free spot for this vehicle");
}
synchronized Ticket leave(String ticketId) {
Ticket t = tickets.get(ticketId);
if (t == null) throw new ParkingException("unknown ticket");
if (t.paid()) throw new ParkingException("ticket already paid");
Instant exit = now.get();
Ticket done = new Ticket(t.id(), t.plate(), t.vehicle(), t.spotId(), t.entry(), exit,
pricing.fee(t.vehicle(), Duration.between(t.entry(), exit)), true);
tickets.put(done.id(), done);
spots.get(done.spotId()).plate = null;
parked.remove(done.plate());
return done;
}
synchronized Map<SpotSize, Integer> free() {
Map<SpotSize, Integer> out = new EnumMap<>(SpotSize.class);
for (SpotSize s : SpotSize.values()) out.put(s, 0);
for (Spot s : spots.values()) if (s.plate == null) out.merge(s.size, 1, Integer::sum);
return out;
}
}
public static void main(String[] args) {
Instant[] clock = { Instant.parse("2026-11-07T09:00:00Z") };
Lot lot = new Lot(List.of(new Level(1, 1, 2, 1), new Level(2, 0, 1, 1)),
new HourlyPricing(Map.of(VehicleType.MOTORCYCLE, 20, VehicleType.CAR, 50, VehicleType.TRUCK, 100)),
() -> clock[0]);
var M = VehicleType.MOTORCYCLE; var C = VehicleType.CAR; var T = VehicleType.TRUCK;
List<Vehicle> arrivals = List.of(new Vehicle("BIKE-1", M), new Vehicle("CAR-A", C), new Vehicle("CAR-B", C),
new Vehicle("TRUCK-1", T), new Vehicle("CAR-C", C), new Vehicle("CAR-D", C), new Vehicle("TRUCK-2", T));
Map<String, String> tickets = new HashMap<>();
for (Vehicle v : arrivals) {
String name = v.type().name().toLowerCase();
try {
Ticket t = lot.park(v);
tickets.put(v.plate(), t.id());
System.out.printf("%-8s %-10s -> %s (ticket %s)%n", v.plate(), name, t.spotId(), t.id());
} catch (ParkingException e) {
System.out.printf("%-8s %-10s -> %s%n", v.plate(), name, e.getMessage());
}
}
clock[0] = clock[0].plus(Duration.ofMinutes(150));
Ticket t = lot.leave(tickets.get("CAR-A"));
long mins = Duration.between(t.entry(), t.exit()).toMinutes();
System.out.printf("CAR-A left after %dh%dm0s, fee %d taka%n", mins / 60, mins % 60, t.fee());
try { lot.leave(tickets.get("CAR-A")); } catch (ParkingException e) { System.out.println("CAR-A again: " + e.getMessage()); }
try { lot.park(new Vehicle("TRUCK-2", T)); } catch (ParkingException e) { System.out.println("TRUCK-2 retry -> " + e.getMessage()); }
var f = lot.free();
System.out.printf("free: small=%d medium=%d large=%d%n", f.get(SpotSize.SMALL), f.get(SpotSize.MEDIUM), f.get(SpotSize.LARGE));
}
}#include <chrono>
#include <cstdio>
#include <functional>
#include <map>
#include <memory>
#include <mutex>
#include <optional>
#include <stdexcept>
#include <string>
#include <vector>
using Clock = std::function<std::chrono::minutes()>; // minutes since opening
enum class VehicleType { Motorcycle, Car, Truck };
enum class SpotSize { Small, Medium, Large }; // enum class compares by value: Large > Small
const char* name(VehicleType t) {
switch (t) {
case VehicleType::Motorcycle: return "motorcycle";
case VehicleType::Car: return "car";
default: return "truck";
}
}
SpotSize sizeFor(VehicleType t) {
switch (t) {
case VehicleType::Motorcycle: return SpotSize::Small;
case VehicleType::Car: return SpotSize::Medium;
default: return SpotSize::Large;
}
}
struct Vehicle { std::string plate; VehicleType type; };
struct Spot { std::string id; SpotSize size; std::string plate; }; // "" = free
struct Ticket {
std::string id, plate, spotId;
VehicleType vehicle;
std::chrono::minutes entry{}, exit{};
int fee = 0;
bool paid = false;
};
struct ParkingError : std::runtime_error { using std::runtime_error::runtime_error; };
struct PricingStrategy {
virtual ~PricingStrategy() = default;
virtual int fee(VehicleType v, std::chrono::minutes stay) const = 0;
};
struct HourlyPricing : PricingStrategy {
std::map<VehicleType, int> rates;
explicit HourlyPricing(std::map<VehicleType, int> r) : rates(std::move(r)) {}
int fee(VehicleType v, std::chrono::minutes stay) const override {
long hours = std::max<long>(1, (stay.count() + 59) / 60);
return static_cast<int>(hours) * rates.at(v);
}
};
struct Level {
std::vector<Spot> spots;
Level(int n, int small, int medium, int large) {
int i = 0;
auto add = [&](int count, SpotSize s) {
for (int k = 0; k < count; ++k) spots.push_back({"L" + std::to_string(n) + "-S" + std::to_string(++i), s, ""});
};
add(small, SpotSize::Small); add(medium, SpotSize::Medium); add(large, SpotSize::Large);
}
Spot* findSpot(SpotSize need) {
for (auto& s : spots) if (s.plate.empty() && s.size >= need) return &s;
return nullptr;
}
};
class Lot {
public:
Lot(std::vector<Level> levels, std::unique_ptr<PricingStrategy> pricing, Clock now)
: levels_(std::move(levels)), pricing_(std::move(pricing)), now_(std::move(now)) {}
// Returns a copy: the caller can't reach into the lot's own ticket.
Ticket park(const Vehicle& v) {
if (v.plate.empty()) throw std::invalid_argument("vehicle needs a plate");
std::lock_guard lock(mu_); // RAII: unlocked on every return and throw
if (parked_.count(v.plate)) throw ParkingError("vehicle is already parked");
for (auto& lv : levels_) {
Spot* s = lv.findSpot(sizeFor(v.type));
if (!s) continue;
s->plate = v.plate;
char id[16];
std::snprintf(id, sizeof id, "T%04d", ++next_);
Ticket t{id, v.plate, s->id, v.type, now_()};
tickets_[t.id] = t;
parked_[v.plate] = t.id;
return t;
}
throw ParkingError("no free spot for this vehicle");
}
Ticket leave(const std::string& ticketId) {
std::lock_guard lock(mu_);
auto it = tickets_.find(ticketId);
if (it == tickets_.end()) throw ParkingError("unknown ticket");
Ticket& t = it->second;
if (t.paid) throw ParkingError("ticket already paid");
t.exit = now_();
t.fee = pricing_->fee(t.vehicle, t.exit - t.entry);
t.paid = true;
for (auto& lv : levels_) for (auto& s : lv.spots) if (s.id == t.spotId) s.plate.clear();
parked_.erase(t.plate);
return t;
}
std::map<SpotSize, int> free() {
std::lock_guard lock(mu_);
std::map<SpotSize, int> out{{SpotSize::Small, 0}, {SpotSize::Medium, 0}, {SpotSize::Large, 0}};
for (auto& lv : levels_) for (auto& s : lv.spots) if (s.plate.empty()) ++out[s.size];
return out;
}
private:
std::mutex mu_;
std::vector<Level> levels_;
std::unique_ptr<PricingStrategy> pricing_;
Clock now_;
std::map<std::string, Ticket> tickets_;
std::map<std::string, std::string> parked_; // plate -> ticket id
int next_ = 0;
};
int main() {
using namespace std::chrono_literals;
std::chrono::minutes clock = 0min;
Lot lot({Level(1, 1, 2, 1), Level(2, 0, 1, 1)},
std::make_unique<HourlyPricing>(std::map<VehicleType, int>{
{VehicleType::Motorcycle, 20}, {VehicleType::Car, 50}, {VehicleType::Truck, 100}}),
[&] { return clock; });
auto M = VehicleType::Motorcycle, C = VehicleType::Car, T = VehicleType::Truck;
std::vector<Vehicle> arrivals{{"BIKE-1", M}, {"CAR-A", C}, {"CAR-B", C}, {"TRUCK-1", T},
{"CAR-C", C}, {"CAR-D", C}, {"TRUCK-2", T}};
std::map<std::string, std::string> tickets;
for (auto& v : arrivals) {
try {
Ticket t = lot.park(v);
tickets[v.plate] = t.id;
std::printf("%-8s %-10s -> %s (ticket %s)\n", v.plate.c_str(), name(v.type), t.spotId.c_str(), t.id.c_str());
} catch (const ParkingError& e) {
std::printf("%-8s %-10s -> %s\n", v.plate.c_str(), name(v.type), e.what());
}
}
clock += 150min;
Ticket t = lot.leave(tickets["CAR-A"]);
long mins = (t.exit - t.entry).count();
std::printf("CAR-A left after %ldh%ldm0s, fee %d taka\n", mins / 60, mins % 60, t.fee);
try { lot.leave(tickets["CAR-A"]); } catch (const ParkingError& e) { std::printf("CAR-A again: %s\n", e.what()); }
try { lot.park({"TRUCK-2", T}); } catch (const ParkingError& e) { std::printf("TRUCK-2 retry -> %s\n", e.what()); }
auto f = lot.free();
std::printf("free: small=%d medium=%d large=%d\n", f[SpotSize::Small], f[SpotSize::Medium], f[SpotSize::Large]);
}How to run each one: python3 parking.py, node parking.ts (Node 23+ runs TypeScript directly), rustc --edition 2021 parking.rs && ./parking, javac Parking.java && java Parking (Java 21), g++ -std=c++20 parking.cpp -o parking && ./parking.
What changes from language to language, and why:
| How "pricing is swappable" is written | How the ticket is kept safe | How the lock works | |
|---|---|---|---|
| Go | an interface, satisfied implicitly | return a struct value, so the caller gets a copy | sync.Mutex + defer Unlock() |
| Python | a Protocol: any object with a fee method fits | @dataclass(frozen=True); replace() makes the paid copy | threading.Lock with with |
| TypeScript | an interface | readonly fields; Leave returns a new object | no lock: JS runs this on one thread, and park has no await, so nothing can cut in halfway |
| Rust | a trait object Box<dyn PricingStrategy>, marked Send + Sync | return a clone(); nobody gets a reference into the lot | all changing state lives inside the Mutex, so the compiler won't let you touch a spot without the lock |
| Java | an interface | a record is immutable; Leave builds a new one | synchronized methods share the object's lock |
| C++ | an abstract class with a pure virtual fee | return by value | std::lock_guard (RAII): unlocked on every return and every throw |
The Rust row is worth a second look. In Go, Java and C++ the lock and the data it protects are two separate fields, and only discipline keeps them together. In Rust the data is inside the mutex, so forgetting the lock is a compile error, not a bug you find in production.
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]
What each test proves:
| Test | Checks |
|---|---|
TestBestFitSpot | Smallest fitting spot first, then the next size up when that's taken |
TestLowerLevelFirst | Level 1 fills before Level 2 |
TestTruckNeedsLarge | A vehicle never gets a smaller spot, even when many small ones are free |
TestInvalidInput | Empty plate and unknown ticket are rejected with the right error |
TestSamePlateTwice | One plate can't hold two spots |
TestFeeRoundsUpHours | Boundaries: 0 minutes, exactly 1 hour, 1 hour 1 minute, 2h30m |
TestLeaveFreesSpotOnce | Leave frees the spot, a second Leave is refused, and the spot is reused |
TestReturnedTicketIsACopy | Changing the returned ticket doesn't change the lot's ticket |
TestSwapPricing | A different strategy works with no change to Lot |
TestConcurrentGates | 100 goroutines, 50 spots: exactly 50 park and no spot is given twice |
Does the concurrency test actually catch anything? We checked: delete the two lock lines from Park and run it again.
$ go test -race -run Concurrent .
WARNING: DATA RACE
...
FAIL parking 0.015s
The race detector reported data races on every run (5 to 8 in our runs) and the test failed. Put the lock back and it passes. A test that passes with and without the fix proves nothing, so it's worth checking this once.
Design review
Reviewing this as if a teammate sent it for review:
What's good
- Each type has one job, and the only place with decisions is
Lot. - Swapping the pricing rule or the clock needs zero edits to
Lot. - The lock can't be skipped by accident: spot state is private, and every public method takes the lock.
- Tests run in milliseconds and cover the money edge cases and the race.
What could break, and what happens when requirements change
| "What if…" | What breaks | What to change |
|---|---|---|
Cars keep taking large spots (look at CAR-D and TRUCK-2 in the demo) | Trucks get turned away while cars sit in truck spots | Make spot choice a second strategy: "exact size only for trucks", or keep the last N large spots for trucks. This is the moment a SpotFinder interface earns its place, not before. |
| 10,000 spots and 20 gates | One lock makes every gate wait for the whole scan | Keep a free list per size per level (a queue of free spot IDs), and lock per level. Park becomes O(levels) instead of O(spots). |
| Electric car charging spots | SpotSize is a single ordered line; "EV" isn't bigger or smaller | Add spot features (a set: ev, handicapped) next to size, and match on both. |
| Lost ticket | Leave needs a ticket ID | Look up the active ticket by plate (we already keep parked[plate]), and charge a lost-ticket fee as a pricing rule. |
| Reservations | A free spot might be promised to someone | A spot gets a third state, reserved, with an expiry. Now it's a small state machine. |
| Power cut | Everything is in memory; all tickets vanish | Persist tickets and spot state. That's the HLD version: a database, and the lot becomes a service. |
What is deliberately simple
- Ticket IDs are a counter, which is fine in one process. Across several servers you'd use UUIDs or a central ID service.
- The free count scans every spot. For a gate board refreshed every few seconds, that's fine.
What I'd change before production: the spot-choice rule from the first row (it's a real complaint in real car parks), and logging every Park and Leave with the ticket ID, so a disputed fee can be traced.
Interview questions
Practice questions
Beginner
- Why is
Spotnot responsible for finding a free spot? - Why does
Parkreturn aTicketvalue instead of a pointer? - How does the code guarantee "smallest spot that fits"?
Intermediate
- How would you add a night rate, cheaper between 10 p.m. and 6 a.m.?
- Two gates call
Parkat once for the last spot. Walk through what happens. - How do you test that a 2h30m stay costs 150 taka without waiting 2h30m?
Advanced
- The lot grows to 10,000 spots and 20 gates. Where is the bottleneck, and how do you fix it?
- Trucks are being turned away while cars use large spots. Change the design so the rule is configurable.
- Why would a Singleton
Lotmake this code worse?
Follow-ups interviewers like
- "Add a motorcycle-only floor." · "Show free spots per level, not just per size." · "Make the fee depend on the level (rooftop is cheaper)."
Common design mistakes
- Subclass per spot size (
SmallSpot,LargeSpot) when sizes differ only in data. - Pricing written inside
Leavewithifchains, so every rule change edits parking code. - Calling the system clock directly, which makes fee tests slow or flaky.
- Forgetting concurrency entirely, or locking only the "find" step and not the "mark taken" step, which leaves the race in place.
- Returning internal pointers, so callers can change a ticket or a spot without the lock.
- A
Gateclass that only forwards calls, which adds a layer with no decision in it.
Answers
1. Why isn't Spot responsible for finding a free spot?
Finding needs to see all spots and compare them. A single spot only knows itself. If Spot searched, it would need a reference to its siblings, and the "best fit" rule would be spread over many objects. The search belongs to whoever can see the whole floor (Level) and the whole lot (Lot).
2. Why does Park return a value, not a pointer?
A pointer would let the caller change the lot's own ticket, for example setting Status = Paid, without the lock and without freeing the spot. A value is a copy: the caller can read it and scribble on it, and the lot's original stays correct. TestReturnedTicketIsACopy checks exactly this.
3. How does the code guarantee smallest-fit?
NewLevel adds spots smallest first. findSpot returns the first free spot with Size >= need. Because of the order, the first match is also the smallest one that fits. The guarantee lives in the constructor's ordering, so a comment there matters.
4. How would you add a night rate?
Write a new type with a Fee method that splits the stay into night and day hours and prices each, then pass it to NewLot. Lot doesn't change. One catch: Fee gets only the duration, and a night rate needs the actual times, so you'd change the interface to Fee(vehicle, entry, exit). Saying that out loud in an interview is a plus: it shows you noticed the interface is missing information.
5. Two gates, one spot: what happens?
Both call Park. One gets mu first, finds the spot, marks it taken, creates the ticket and unlocks. The other was blocked on Lock(); it now scans, finds nothing and returns ErrLotFull. The key point is that "find" and "mark taken" happen inside the same critical section. Locking them separately would let both find the same free spot.
6. How do you test a 2h30m stay instantly?
The lot never calls time.Now() directly; it calls l.now(), which defaults to time.Now but can be replaced with WithClock. The test passes a fake clock, parks, moves the fake clock forward 150 minutes and calls Leave. No sleeping, and the result is the same on every run.
7. 10,000 spots and 20 gates: where's the bottleneck?
Two things: the scan is O(spots) per arrival, and it all happens under one lock, so gates queue. Fix the scan with a free list per size per level: a queue of free spot IDs, so finding a spot is popping from a queue. Fix contention by locking per level instead of per lot. A gate only blocks other gates working on the same level. The free count becomes a few counters, kept up to date on Park and Leave.
8. Make the spot rule configurable
Pull "which spot for this vehicle" behind a second interface, say SpotFinder with Find(levels, vehicle) *Spot. The current behaviour becomes BestFitFinder. Add ExactSizeForTrucks or ReserveLargeSpots{Keep: 2} as new types. This is the same Strategy move as pricing, made when a real need appears rather than up front.
9. Why would a Singleton lot be worse?
Every test would share one global lot, so tests could no longer run in parallel or start clean. A second building would need a rewrite. And the dependency becomes hidden: functions grab the global instead of receiving a *Lot, so you can't see from a signature what touches parking state. Creating one Lot in main and passing it gives the "only one" property without any of that.
What interviewers look for: clear entities with one job each, the pricing rule behind an interface, a real answer for concurrency (where the lock goes and why), and how you handle "what if" questions without rewriting everything.
Designed it yourself and understood the solution? Mark it to track your progress.