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
1 from __future__ import annotations
2 from abc import ABC, abstractmethod
3 from enum import Enum
4 from datetime import datetime
5 from dataclasses import dataclass, field
6 from typing import Protocol
7 from threading import Lock
8 import uuid
9
10
11 class VehicleType(Enum):
12 MOTORCYCLE = "motorcycle"
13 CAR = "car"
14 TRUCK = "truck"
15
16 class SpotType(Enum):
17 SMALL = "small" # motorcycles
18 MEDIUM = "medium" # cars
19 LARGE = "large" # trucks (also fits smaller vehicles)
20
21
22 class Vehicle(ABC):
23 def __init__(self, plate: str):
24 self.plate = plate
25
26 @abstractmethod
27 def required_spot_type(self) -> SpotType: ...
28
29 @property
30 @abstractmethod
31 def vehicle_type(self) -> VehicleType: ...
32
33 class Motorcycle(Vehicle):
34 def required_spot_type(self) -> SpotType:
35 return SpotType.SMALL
36 @property
37 def vehicle_type(self) -> VehicleType:
38 return VehicleType.MOTORCYCLE
39
40 class Car(Vehicle):
41 def required_spot_type(self) -> SpotType:
42 return SpotType.MEDIUM
43 @property
44 def vehicle_type(self) -> VehicleType:
45 return VehicleType.CAR
46
47 class Truck(Vehicle):
48 def required_spot_type(self) -> SpotType:
49 return SpotType.LARGE
50 @property
51 def vehicle_type(self) -> VehicleType:
52 return VehicleType.TRUCK
53
54
55 VEHICLE_FACTORY: dict[VehicleType, type[Vehicle]] = {
56 VehicleType.MOTORCYCLE: Motorcycle,
57 VehicleType.CAR: Car,
58 VehicleType.TRUCK: Truck,
59 }
60
61 def create_vehicle(vtype: VehicleType, plate: str) -> Vehicle:
62 return VEHICLE_FACTORY[vtype](plate)
63
64
65 class ParkingSpot:
66 def __init__(self, spot_id: str, spot_type: SpotType):
67 self.id = spot_id
68 self.type = spot_type
69 self._vehicle: Vehicle | None = None
70
71 @property
72 def is_available(self) -> bool:
73 return self._vehicle is None
74
75 def assign(self, vehicle: Vehicle) -> None:
76 if not self.is_available:
77 raise RuntimeError(f"Spot {self.id} already occupied")
78 self._vehicle = vehicle
79
80 def release(self) -> Vehicle:
81 if self._vehicle is None:
82 raise RuntimeError(f"Spot {self.id} is empty")
83 v, self._vehicle = self._vehicle, None
84 return v
85
86
87 class ParkingFloor:
88 def __init__(self, floor_number: int, spots: list[ParkingSpot]):
89 self.floor_number = floor_number
90 self._spots_by_type: dict[SpotType, list[ParkingSpot]] = {}
91 for spot in spots:
92 self._spots_by_type.setdefault(spot.type, []).append(spot)
93
94 def find_available(self, spot_type: SpotType) -> ParkingSpot | None:
95 for spot in self._spots_by_type.get(spot_type, []):
96 if spot.is_available:
97 return spot
98 return None
99
100 def available_count(self, spot_type: SpotType) -> int:
101 return sum(1 for s in self._spots_by_type.get(spot_type, []) if s.is_available)
102
103
104 class ParkingStrategy(Protocol):
105 def find_spot(self, floors: list[ParkingFloor], spot_type: SpotType) -> ParkingSpot | None: ...
106
107 class NearestFirstStrategy:
108 """Assigns closest available spot, lowest floor first."""
109 def find_spot(self, floors: list[ParkingFloor], spot_type: SpotType) -> ParkingSpot | None:
110 for floor in floors:
111 spot = floor.find_available(spot_type)
112 if spot:
113 return spot
114 return None
115
116
117 @dataclass
118 class Ticket:
119 id: str = field(default_factory=lambda: str(uuid.uuid4())[:8])
120 vehicle: Vehicle = field(default=None)
121 spot: ParkingSpot = field(default=None)
122 entry_time: datetime = field(default_factory=datetime.now)
123
124
125 class ParkingLot:
126 def __init__(self, floors: list[ParkingFloor], strategy: ParkingStrategy | None = None):
127 self._floors = floors
128 self._strategy = strategy or NearestFirstStrategy()
129 self._active_tickets: dict[str, Ticket] = {}
130 self._lock = Lock()
131
132 def park(self, vehicle: Vehicle) -> Ticket:
133 with self._lock:
134 spot = self._strategy.find_spot(self._floors, vehicle.required_spot_type())
135 if spot is None:
136 raise RuntimeError(f"No {vehicle.required_spot_type().value} spot available")
137 spot.assign(vehicle)
138 ticket = Ticket(vehicle=vehicle, spot=spot)
139 self._active_tickets[ticket.id] = ticket
140 return ticket
141
142 def unpark(self, ticket_id: str) -> Vehicle:
143 with self._lock:
144 ticket = self._active_tickets.pop(ticket_id, None)
145 if ticket is None:
146 raise ValueError("Invalid ticket")
147 return ticket.spot.release()
148
149 def availability(self) -> dict[str, dict[str, int]]:
150 return {
151 f"Floor {f.floor_number}": {
152 st.value: f.available_count(st) for st in SpotType
153 }
154 for f in self._floors
155 }
156
157
158 if __name__ == "__main__":
159 floors = [
160 ParkingFloor(1, [ParkingSpot(f"1-S{i}", SpotType.SMALL) for i in range(5)]
161 + [ParkingSpot(f"1-M{i}", SpotType.MEDIUM) for i in range(10)]
162 + [ParkingSpot(f"1-L{i}", SpotType.LARGE) for i in range(3)]),
163 ParkingFloor(2, [ParkingSpot(f"2-M{i}", SpotType.MEDIUM) for i in range(10)]
164 + [ParkingSpot(f"2-L{i}", SpotType.LARGE) for i in range(5)]),
165 ]
166 lot = ParkingLot(floors)
167
168 t1 = lot.park(Car("KA-01-1234"))
169 t2 = lot.park(Truck("KA-02-5678"))
170 t3 = lot.park(Motorcycle("KA-03-9999"))
171 print("After parking 3 vehicles:")
172 for floor_name, counts in lot.availability().items():
173 print(f" {floor_name}: {counts}")
174
175 lot.unpark(t1.id)
176 print("\nAfter unparking the car:")
177 for floor_name, counts in lot.availability().items():
178 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.