The analogy: a physical slot allocator
Think of a parking lot like a physical mail room in a massive skyscraper. Mail slots (parking spots) come in different sizes: small slots for letters (motorcycles), medium slots for packages (cars), and large bins for crates (buses).
The clerk at the front desk (the ParkingLot system) has to allocate these slots.
If a package (car) arrives, the clerk looks at the floor plans (levels) to find the nearest open slot of the correct size. Once allocated, a receipt (ticket) is issued.
If two clerks try to assign the same slot at the exact same moment (concurrency race), they might write over each other, leaving two packages jammed in one box.
Our job is to design the classes, their relationships, and the allocation policies to make this system durable, extensible, and thread-safe.
How to run an OOD question
It's not about UML perfection — it's about clean responsibilities and extensibility. Steps: clarify scope → identify entities (nouns) → assign responsibilities (verbs) → relationships → call out where behavior varies (Strategy) → walk a scenario.
Clarify first: multiple levels? vehicle sizes (motorcycle/car/bus)? pricing? payment? Pin scope: "single lot, multiple levels, three spot sizes, hourly pricing, ignore payments — OK?"
Entities & relationships
ParkingLotis the facade:parkVehicle/unpark. It owns levels and issues tickets.Levelowns spots and answersfindSpot(vehicle).ParkingSpothas a size andcanFit(vehicle); subclasses or asizeenum (prefer the enum +canFitover a deep spot hierarchy).Vehiclehierarchy (Car/Motorcycle/Bus) carries size.Ticketrecords spot + entry time for fee calculation.
Where the patterns live
- Strategy — spot assignment. "nearest", "first-available", "fit smallest
spot" are interchangeable
SpotAssignmentStrategyimplementations injected intoLevel. New policy = new class, no edits (Open/Closed). - Strategy — pricing.
FeeStrategy(flat, hourly, tiered) computed from the ticket. - Singleton (maybe). One
ParkingLotinstance — but inject it rather than reaching for a global.
Modeling every spot/vehicle combination as a subclass blows up fast. A size
enum plus spot.canFit(vehicle) keeps it small and handles "a car can use a
large spot if no compact is free" without new types.
Here's that spot-assignment strategy running. A motorcycle fits anywhere; a car needs a compact-or-larger spot; a large vehicle needs a large spot. Watch the scan reject spots that are taken or too small, then take the first that fits — swap the operations to test "two cars, one compact left".
dashed = free · a vehicle takes the nearest spot ranked its size or larger
1/287 spots: two motorcycle (M), three compact (C), two large (L). A vehicle parks in the nearest spot it fits — bigger spots accept smaller vehicles, never the reverse.
Think it through like the interview
Before reading further, try to run the method yourself — this widget walks the exact five moves, one at a time:
PROBLEMDesign the classes for a parking lot: vehicles arrive, get a spot that fits, receive a ticket, and pay on exit. You have ~35 minutes and a whiteboard.
- 1
Clarify scope
“What would change my design the most? Ask THOSE questions first.”
- 2
Nouns → entities
“Read the problem statement back. Which nouns survive as classes?”
unlocks after the stage above - 3
Verbs → responsibilities
“Who owns parkVehicle? Who owns findSpot? Why not all on ParkingLot?”
unlocks after the stage above - 4
Spot what varies → patterns
“Which decisions might change next month without the structure changing?”
unlocks after the stage above - 5
Walk a scenario + break it
“Narrate 'a car arrives'. Then: two cars, one spot left. What breaks?”
unlocks after the stage above
Walk a scenario + concurrency
"A car arrives": ParkingLot.parkVehicle(car) → ask each Level for a spot via
its assignment strategy → mark spot occupied → issue Ticket. Concurrency
follow-up: two cars racing for the last spot is the same race as a balance
double-spend — guard spot acquisition atomically (compare-and-set on the spot's
state, or a lock per level) so a spot is never double-assigned.
Code Implementation
Below are working class templates for a thread-safe Parking Lot in Python, Java, and C++:
1. Python
from abc import ABC, abstractmethod
from enum import Enum
import threading
from typing import List, Optional
class VehicleSize(Enum):
MOTORCYCLE = 1
COMPACT = 2
LARGE = 3
class Vehicle(ABC):
def __init__(self, size: VehicleSize, license_plate: str):
self.size = size
self.license_plate = license_plate
class Motorcycle(Vehicle):
def __init__(self, license_plate: str):
super().__init__(VehicleSize.MOTORCYCLE, license_plate)
class Car(Vehicle):
def __init__(self, license_plate: str):
super().__init__(VehicleSize.COMPACT, license_plate)
class Bus(Vehicle):
def __init__(self, license_plate: str):
super().__init__(VehicleSize.LARGE, license_plate)
class ParkingSpot:
def __init__(self, spot_number: int, size: VehicleSize):
self.spot_number = spot_number
self.size = size
self.vehicle: Optional[Vehicle] = None
def is_free(self) -> bool:
return self.vehicle is None
def can_fit(self, vehicle: Vehicle) -> bool:
return self.is_free() and self.size.value >= vehicle.size.value
def park(self, vehicle: Vehicle) -> bool:
if not self.can_fit(vehicle):
return False
self.vehicle = vehicle
return True
def remove_vehicle(self):
self.vehicle = None
class SpotAssignmentStrategy(ABC):
@abstractmethod
def find_spot(self, spots: List[ParkingSpot], vehicle: Vehicle) -> Optional[ParkingSpot]:
pass
class FirstAvailableStrategy(SpotAssignmentStrategy):
def find_spot(self, spots: List[ParkingSpot], vehicle: Vehicle) -> Optional[ParkingSpot]:
for spot in spots:
if spot.can_fit(vehicle):
return spot
return None
class Level:
def __init__(self, floor_number: int, spots: List[ParkingSpot], strategy: SpotAssignmentStrategy):
self.floor_number = floor_number
self.spots = spots
self.strategy = strategy
self._lock = threading.Lock() # Lock level to prevent reservation races
def park_vehicle(self, vehicle: Vehicle) -> bool:
with self._lock: # Atomic check-and-reserve
spot = self.strategy.find_spot(self.spots, vehicle)
if spot:
return spot.park(vehicle)
return False
def unpark_vehicle(self, vehicle: Vehicle) -> bool:
with self._lock:
for spot in self.spots:
if spot.vehicle == vehicle:
spot.remove_vehicle()
return True
return False
2. Java
import java.util.List;
import java.util.Optional;
import java.util.concurrent.locks.ReentrantLock;
enum VehicleSize {
MOTORCYCLE(1), COMPACT(2), LARGE(3);
private final int value;
VehicleSize(int val) { this.value = val; }
public int getValue() { return value; }
}
abstract class Vehicle {
private final VehicleSize size;
private final String licensePlate;
public Vehicle(VehicleSize size, String lp) { this.size = size; this.licensePlate = lp; }
public VehicleSize getSize() { return size; }
}
class Car extends Vehicle {
public Car(String lp) { super(VehicleSize.COMPACT, lp); }
}
class ParkingSpot {
private final int spotNumber;
private final VehicleSize size;
private Vehicle vehicle;
public ParkingSpot(int spotNumber, VehicleSize size) {
this.spotNumber = spotNumber;
this.size = size;
}
public synchronized boolean isFree() { return vehicle == null; }
public synchronized boolean canFit(Vehicle v) {
return isFree() && size.getValue() >= v.getSize().getValue();
}
public synchronized boolean park(Vehicle v) {
if (!canFit(v)) return false;
this.vehicle = v;
return true;
}
public synchronized void removeVehicle() { this.vehicle = null; }
public synchronized Vehicle getVehicle() { return vehicle; }
}
interface SpotAssignmentStrategy {
Optional<ParkingSpot> findSpot(List<ParkingSpot> spots, Vehicle vehicle);
}
class FirstAvailableStrategy implements SpotAssignmentStrategy {
@Override
public Optional<ParkingSpot> findSpot(List<ParkingSpot> spots, Vehicle v) {
return spots.stream().filter(spot -> spot.canFit(v)).findFirst();
}
}
class Level {
private final int floorNumber;
private final List<ParkingSpot> spots;
private final SpotAssignmentStrategy strategy;
private final ReentrantLock lock = new ReentrantLock();
public Level(int floorNumber, List<ParkingSpot> spots, SpotAssignmentStrategy strat) {
this.floorNumber = floorNumber;
this.spots = spots;
this.strategy = strat;
}
public boolean parkVehicle(Vehicle v) {
lock.lock();
try {
Optional<ParkingSpot> spot = strategy.findSpot(spots, v);
if (spot.isPresent()) {
return spot.get().park(v);
}
return false;
} finally {
lock.unlock();
}
}
}
3. C++
#include <vector>
#include <string>
#include <memory>
#include <mutex>
#include <algorithm>
enum class VehicleSize { MOTORCYCLE = 1, COMPACT = 2, LARGE = 3 };
class Vehicle {
private:
VehicleSize size;
std::string license_plate;
protected:
Vehicle(VehicleSize sz, std::string lp) : size(sz), license_plate(lp) {}
public:
virtual ~Vehicle() = default;
VehicleSize getSize() const { return size; }
};
class Car : public Vehicle {
public:
Car(std::string lp) : Vehicle(VehicleSize::COMPACT, lp) {}
};
class ParkingSpot {
private:
int spot_number;
VehicleSize size;
std::shared_ptr<Vehicle> vehicle;
std::mutex spot_mutex;
public:
ParkingSpot(int num, VehicleSize sz) : spot_number(num), size(sz), vehicle(nullptr) {}
bool isFree() {
std::lock_guard<std::mutex> lock(spot_mutex);
return vehicle == nullptr;
}
bool canFit(const Vehicle& v) {
std::lock_guard<std::mutex> lock(spot_mutex);
return vehicle == nullptr && static_cast<int>(size) >= static_cast<int>(v.getSize());
}
bool park(std::shared_ptr<Vehicle> v) {
std::lock_guard<std::mutex> lock(spot_mutex);
if (vehicle != nullptr || static_cast<int>(size) < static_cast<int>(v->getSize())) {
return false;
}
vehicle = v;
return true;
}
void removeVehicle() {
std::lock_guard<std::mutex> lock(spot_mutex);
vehicle = nullptr;
}
};
class SpotAssignmentStrategy {
public:
virtual ~SpotAssignmentStrategy() = default;
virtual std::shared_ptr<ParkingSpot> findSpot(const std::vector<std::shared_ptr<ParkingSpot>>& spots, const Vehicle& vehicle) = 0;
};
class FirstAvailableStrategy : public SpotAssignmentStrategy {
public:
std::shared_ptr<ParkingSpot> findSpot(const std::vector<std::shared_ptr<ParkingSpot>>& spots, const Vehicle& vehicle) override {
for (auto spot : spots) {
if (spot->canFit(vehicle)) return spot;
}
return nullptr;
}
};
class Level {
private:
int floor_number;
std::vector<std::shared_ptr<ParkingSpot>> spots;
std::shared_ptr<SpotAssignmentStrategy> strategy;
std::mutex level_mutex;
public:
Level(int num, std::vector<std::shared_ptr<ParkingSpot>> s, std::shared_ptr<SpotAssignmentStrategy> strat)
: floor_number(num), spots(s), strategy(strat) {}
bool parkVehicle(std::shared_ptr<Vehicle> v) {
std::lock_guard<std::mutex> lock(level_mutex); // Lock level to serialize search + reserve
auto spot = strategy->findSpot(spots, *v);
if (spot) {
return spot->park(v);
}
return false;
}
};
Check yourself
1. Why do we place the Lock/Mutex at the Level level instead of inside the ParkingSpot class during the find-and-reserve operation?
2. How does using the Strategy pattern for spot assignment satisfy the SOLID principles?
3. Why is a VehicleSize enum preferred over creating a deep subclass hierarchy for every combination of spot and vehicle type (e.g. MotorcycleSpot, CompactSpot, LargeSpot)?
Practice — level up
A parking lot is an allocation problem: hand out a limited resource, take it back, and enforce "one holder at a time". These drills are the same shape.
Climb in order — every rung assumes the one above it. Solve on LeetCode, then tick it here; progress is saved on this device.
Warm-up — count what's free
Allocation with nothing but counters.Per-size counters — the simplest spot allocation.
Core — allocate & release specific slots
Now identity matters: which spot, given back.- Seat Reservation ManagerMedium
Reserve / unreserve numbered slots — parking spots by another name.
- Design Circular QueueMedium
Fixed capacity with free/occupied bookkeeping and wrap-around.
Stretch — paired entry/exit + fees
Ticket on the way in, charge on the way out.Check-in/check-out pairs and aggregate on exit — the ticket-and-fee half of the lot.