Parking Lot
Multi-floor, multi-vehicle-type parking with real-time availability. Layered domain model where each class owns exactly one responsibility.
Key Abstractions
Facade, single entry point for park/unpark operations
Manages spots on one level, knows availability by type
Individual slot that knows its type, size, and occupant
Abstract base. Car, Truck, Motorcycle with different size requirements.
Spot selection algorithm: nearest, spread-out, or type-optimized
Proof of parking, links vehicle to spot with entry time
Class Diagram
The Key Insight
People look at this problem and think it's about assigning cars to spots. It's not, really. It's about building a layered domain model where each class owns exactly one thing. ParkingLot doesn't know about individual spots. It delegates to floors. Floors don't decide strategy. They just report availability. The allocation algorithm lives in a Strategy object you can swap without touching any domain class.
The typical approach is to make ParkingLot a God class with nested loops scanning all spots. That works for 20 spots. It falls apart at 2,000 spots across 5 floors when you need per-floor availability dashboards updating in real time.
Requirements
Functional
- Multi-floor parking with different spot types (small, medium, large)
- Park a vehicle and receive a ticket
- Unpark with a ticket and release the spot
- Query real-time availability per floor and spot type
- Different vehicle types require specific spot sizes
Non-Functional
- Thread-safe for concurrent entry/exit gates
- O(1) availability lookup per floor per type
- Spot assignment strategy must be swappable at runtime
Design Decisions
Why Strategy pattern for spot assignment?
Airport parking wants nearest-to-elevator. Mall parking wants spread-across-floors for even wear. EV parking wants to route to charger-equipped spots first. A strategy interface makes this a config change, not a code change.
Why separate ParkingFloor from ParkingLot?
It mirrors the physical domain. Each floor has its own display board, its own entrance ramp, its own count. Grouping spots by floor gives you O(1) floor-level availability and makes the display board observer straightforward. Without this separation, computing "Floor 3 has 12 medium spots left" means scanning every spot in the building.
Why not allow a Car to park in a LARGE spot?
Some designs allow upsizing (car fits in truck spot). We keep it strict here for simplicity. To add upsizing, modify ParkingStrategy.findSpot() to try the exact type first, then fall back to larger types. The vehicle and spot classes stay the same.
Interview Follow-ups
- "How would you add hourly billing?" Add a
PaymentCalculatorthat takes entry/exit times and a rate card. Theunparkmethod returns aPaymentobject. - "How would you handle EV charging spots?" New
SpotType.EV_MEDIUMand a decorator or subclass ofParkingSpotwith charging state. - "What about reservations?" Add a
ReservationServicethat pre-assigns spots with an expiry. The strategy checks reservations before availability. - "How would you scale to multiple parking lots?" Each lot is independent. A
ParkingLotRegistryaggregates availability across locations for a mobile app.
Code Implementation
from __future__ import annotations
from abc import ABC, abstractmethod
from enum import Enum
from datetime import datetime
from dataclasses import dataclass, field
from typing import Protocol
from threading import Lock
import uuid
class VehicleType(Enum):
MOTORCYCLE = "motorcycle"
CAR = "car"
TRUCK = "truck"
class SpotType(Enum):
SMALL = "small" # motorcycles
MEDIUM = "medium" # cars
LARGE = "large" # trucks (also fits smaller vehicles)
class Vehicle(ABC):
def __init__(self, plate: str):
self.plate = plate
@abstractmethod
def required_spot_type(self) -> SpotType: ...
@property
@abstractmethod
def vehicle_type(self) -> VehicleType: ...
class Motorcycle(Vehicle):
def required_spot_type(self) -> SpotType:
return SpotType.SMALL
@property
def vehicle_type(self) -> VehicleType:
return VehicleType.MOTORCYCLE
class Car(Vehicle):
def required_spot_type(self) -> SpotType:
return SpotType.MEDIUM
@property
def vehicle_type(self) -> VehicleType:
return VehicleType.CAR
class Truck(Vehicle):
def required_spot_type(self) -> SpotType:
return SpotType.LARGE
@property
def vehicle_type(self) -> VehicleType:
return VehicleType.TRUCK
VEHICLE_FACTORY: dict[VehicleType, type[Vehicle]] = {
VehicleType.MOTORCYCLE: Motorcycle,
VehicleType.CAR: Car,
VehicleType.TRUCK: Truck,
}
def create_vehicle(vtype: VehicleType, plate: str) -> Vehicle:
return VEHICLE_FACTORY[vtype](plate)
class ParkingSpot:
def __init__(self, spot_id: str, spot_type: SpotType):
self.id = spot_id
self.type = spot_type
self._vehicle: Vehicle | None = None
@property
def is_available(self) -> bool:
return self._vehicle is None
def assign(self, vehicle: Vehicle) -> None:
if not self.is_available:
raise RuntimeError(f"Spot {self.id} already occupied")
self._vehicle = vehicle
def release(self) -> Vehicle:
if self._vehicle is None:
raise RuntimeError(f"Spot {self.id} is empty")
v, self._vehicle = self._vehicle, None
return v
class ParkingFloor:
def __init__(self, floor_number: int, spots: list[ParkingSpot]):
self.floor_number = floor_number
self._spots_by_type: dict[SpotType, list[ParkingSpot]] = {}
for spot in spots:
self._spots_by_type.setdefault(spot.type, []).append(spot)
def find_available(self, spot_type: SpotType) -> ParkingSpot | None:
for spot in self._spots_by_type.get(spot_type, []):
if spot.is_available:
return spot
return None
def available_count(self, spot_type: SpotType) -> int:
return sum(1 for s in self._spots_by_type.get(spot_type, []) if s.is_available)
class ParkingStrategy(Protocol):
def find_spot(self, floors: list[ParkingFloor], spot_type: SpotType) -> ParkingSpot | None: ...
class NearestFirstStrategy:
"""Assigns closest available spot, lowest floor first."""
def find_spot(self, floors: list[ParkingFloor], spot_type: SpotType) -> ParkingSpot | None:
for floor in floors:
spot = floor.find_available(spot_type)
if spot:
return spot
return None
@dataclass
class Ticket:
id: str = field(default_factory=lambda: str(uuid.uuid4())[:8])
vehicle: Vehicle = field(default=None)
spot: ParkingSpot = field(default=None)
entry_time: datetime = field(default_factory=datetime.now)
class ParkingLot:
def __init__(self, floors: list[ParkingFloor], strategy: ParkingStrategy | None = None):
self._floors = floors
self._strategy = strategy or NearestFirstStrategy()
self._active_tickets: dict[str, Ticket] = {}
self._lock = Lock()
def park(self, vehicle: Vehicle) -> Ticket:
with self._lock:
spot = self._strategy.find_spot(self._floors, vehicle.required_spot_type())
if spot is None:
raise RuntimeError(f"No {vehicle.required_spot_type().value} spot available")
spot.assign(vehicle)
ticket = Ticket(vehicle=vehicle, spot=spot)
self._active_tickets[ticket.id] = ticket
return ticket
def unpark(self, ticket_id: str) -> Vehicle:
with self._lock:
ticket = self._active_tickets.pop(ticket_id, None)
if ticket is None:
raise ValueError("Invalid ticket")
return ticket.spot.release()
def availability(self) -> dict[str, dict[str, int]]:
return {
f"Floor {f.floor_number}": {
st.value: f.available_count(st) for st in SpotType
}
for f in self._floors
}
if __name__ == "__main__":
floors = [
ParkingFloor(1, [ParkingSpot(f"1-S{i}", SpotType.SMALL) for i in range(5)]
+ [ParkingSpot(f"1-M{i}", SpotType.MEDIUM) for i in range(10)]
+ [ParkingSpot(f"1-L{i}", SpotType.LARGE) for i in range(3)]),
ParkingFloor(2, [ParkingSpot(f"2-M{i}", SpotType.MEDIUM) for i in range(10)]
+ [ParkingSpot(f"2-L{i}", SpotType.LARGE) for i in range(5)]),
]
lot = ParkingLot(floors)
t1 = lot.park(Car("KA-01-1234"))
t2 = lot.park(Truck("KA-02-5678"))
t3 = lot.park(Motorcycle("KA-03-9999"))
print("After parking 3 vehicles:")
for floor_name, counts in lot.availability().items():
print(f" {floor_name}: {counts}")
lot.unpark(t1.id)
print("\nAfter unparking the car:")
for floor_name, counts in lot.availability().items():
print(f" {floor_name}: {counts}")Common Mistakes
- ✗Treating ParkingLot as a God class. It should delegate to floors, not manage individual spots.
- ✗Using a flat list of spots instead of floor-based grouping. That kills O(1) availability lookups per floor.
- ✗Hardcoding spot assignment logic makes it impossible to change allocation strategy without rewriting core
- ✗Forgetting concurrency. Two cars arriving at the same time can be assigned the same spot without locks.
Key Points
- ✓Strategy pattern for spot selection: nearest-to-entrance vs spread-load vs type-optimized
- ✓Factory method for vehicle creation avoids switch statements scattered across the codebase
- ✓Observer notifies display boards when availability changes, decoupled from parking logic
- ✓Enum for SpotType, not strings. Compile-time safety on slot size matching.