Problem Statement
Design an object-oriented parking lot system. The lot has multiple levels, each with multiple spots of different sizes (motorcycle, compact, large). Vehicles of various sizes (motorcycle, car, truck) enter, park in the first available suitable spot, and exit with a fee calculated based on parked duration.
Class Hierarchy
from enum import Enum
from dataclasses import dataclass, field
from datetime import datetime
from typing import Optional
import threading
class VehicleSize(Enum):
MOTORCYCLE = 1
COMPACT = 2
LARGE = 3
class SpotSize(Enum):
MOTORCYCLE = 1
COMPACT = 2
LARGE = 3
# Vehicle hierarchy
class Vehicle:
def __init__(self, license_plate: str, size: VehicleSize):
self.license_plate = license_plate
self.size = size
def can_fit_in(self, spot_size: SpotSize) -> bool:
# Vehicle fits if spot is >= vehicle size
return spot_size.value >= self.size.value
class Motorcycle(Vehicle):
def __init__(self, license_plate: str):
super().__init__(license_plate, VehicleSize.MOTORCYCLE)
class Car(Vehicle):
def __init__(self, license_plate: str):
super().__init__(license_plate, VehicleSize.COMPACT)
class Truck(Vehicle):
def __init__(self, license_plate: str):
super().__init__(license_plate, VehicleSize.LARGE)
Spot and Ticket
class ParkingSpot:
def __init__(self, level: int, spot_id: int, size: SpotSize):
self.level = level
self.spot_id = spot_id
self.size = size
self.vehicle: Optional[Vehicle] = None
@property
def is_available(self) -> bool:
return self.vehicle is None
def park(self, vehicle: Vehicle) -> bool:
if not self.is_available or not vehicle.can_fit_in(self.size):
return False
self.vehicle = vehicle
return True
def remove_vehicle(self) -> Optional[Vehicle]:
v = self.vehicle
self.vehicle = None
return v
@dataclass
class Ticket:
ticket_id: str
vehicle: Vehicle
spot: ParkingSpot
entry_time: datetime = field(default_factory=datetime.now)
exit_time: Optional[datetime] = None
def calculate_fee(self, hourly_rates: dict[VehicleSize, float]) -> float:
if self.exit_time is None:
self.exit_time = datetime.now()
duration_hours = (self.exit_time - self.entry_time).total_seconds() / 3600
rate = hourly_rates.get(self.vehicle.size, 5.0)
return round(duration_hours * rate, 2)
Level and Parking Lot
class Level:
def __init__(self, level_id: int, spots_config: dict[SpotSize, int]):
# spots_config = {SpotSize.MOTORCYCLE: 20, SpotSize.COMPACT: 30, SpotSize.LARGE: 10}
self.level_id = level_id
self.spots: list[ParkingSpot] = []
spot_id = 0
for size, count in spots_config.items():
for _ in range(count):
self.spots.append(ParkingSpot(level_id, spot_id, size))
spot_id += 1
def find_available_spot(self, vehicle: Vehicle) -> Optional[ParkingSpot]:
for spot in self.spots:
if spot.is_available and vehicle.can_fit_in(spot.size):
return spot
return None
def available_count(self, size: SpotSize) -> int:
return sum(1 for s in self.spots if s.size == size and s.is_available)
class ParkingLot:
_instance = None
_lock = threading.Lock()
def __new__(cls):
with cls._lock:
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._initialized = False
return cls._instance
def __init__(self):
if self._initialized:
return
self.levels: list[Level] = []
self.active_tickets: dict[str, Ticket] = {} # license_plate -> Ticket
self.hourly_rates = {
VehicleSize.MOTORCYCLE: 2.0,
VehicleSize.COMPACT: 4.0,
VehicleSize.LARGE: 6.0,
}
self._ticket_counter = 0
self._lock = threading.Lock()
self._initialized = True
def add_level(self, level: Level):
self.levels.append(level)
def _generate_ticket_id(self) -> str:
self._ticket_counter += 1
return f"TKT-{self._ticket_counter:06d}"
def park_vehicle(self, vehicle: Vehicle) -> Optional[Ticket]:
with self._lock:
if vehicle.license_plate in self.active_tickets:
print(f"{vehicle.license_plate} already parked.")
return None
for level in self.levels:
spot = level.find_available_spot(vehicle)
if spot:
spot.park(vehicle)
ticket = Ticket(
ticket_id=self._generate_ticket_id(),
vehicle=vehicle,
spot=spot,
)
self.active_tickets[vehicle.license_plate] = ticket
print(f"Parked {vehicle.license_plate} at level {spot.level}, "
f"spot {spot.spot_id} ({spot.size.name})")
return ticket
print(f"No available spot for {vehicle.license_plate}")
return None
def exit_vehicle(self, license_plate: str) -> float:
with self._lock:
ticket = self.active_tickets.pop(license_plate, None)
if not ticket:
print(f"No active ticket for {license_plate}")
return 0.0
ticket.exit_time = datetime.now()
ticket.spot.remove_vehicle()
fee = ticket.calculate_fee(self.hourly_rates)
print(f"Vehicle {license_plate} exited. Fee: $" + f"{fee:.2f}")
return fee
def get_availability(self) -> dict:
with self._lock:
result = {}
for level in self.levels:
result[f"Level {level.level_id}"] = {
size.name: level.available_count(size)
for size in SpotSize
}
return result
Usage Example
def demo():
lot = ParkingLot()
# Configure two levels
lot.add_level(Level(0, {SpotSize.MOTORCYCLE: 5, SpotSize.COMPACT: 10, SpotSize.LARGE: 3}))
lot.add_level(Level(1, {SpotSize.MOTORCYCLE: 5, SpotSize.COMPACT: 10, SpotSize.LARGE: 3}))
# Vehicles arrive
bike = Motorcycle("MOTO-001")
car1 = Car("CAR-001")
truck = Truck("TRUCK-001")
lot.park_vehicle(bike)
lot.park_vehicle(car1)
lot.park_vehicle(truck)
print(lot.get_availability())
# Vehicles exit
lot.exit_vehicle("CAR-001")
lot.exit_vehicle("TRUCK-001")
demo()
Design Decisions and Trade-offs
- Singleton ParkingLot: Thread-safe double-checked locking ensures one lot instance. In a microservice context, replace with a stateless service backed by a database.
- Vehicle fits in larger spot:
can_fit_incompares enum values numerically — a motorcycle (size=1) fits in a compact (size=2) or large (size=3) spot. This maximizes utilization at the cost of leaving large spots for trucks. - Spot selection strategy: The current implementation takes the first available spot (top-to-bottom). Production improvement: prefer the smallest suitable spot (don’t park a motorcycle in a large spot unless necessary). Implement by iterating spots sorted by size.
- Thread safety: A single
threading.Lockguardspark_vehicleandexit_vehicle. For higher concurrency: use per-level locks, or (better) a database with row-level locking and optimistic concurrency. - Fee calculation: Simple hourly rate. Extensions: minimum charge (first 30 minutes free), daily maximums, validation discounts.
Extension: Reservation System
@dataclass
class Reservation:
reservation_id: str
vehicle_size: VehicleSize
start_time: datetime
end_time: datetime
spot: Optional[ParkingSpot] = None
def is_active(self) -> bool:
now = datetime.now()
return self.start_time <= now <= self.end_time
# ParkingLot.reserve_spot() would:
# 1. Find a spot not reserved for the given time window
# 2. Create a Reservation record
# 3. Return reservation_id
# On arrival: look up reservation_id, assign the pre-reserved spot
Interview Checklist
- Clarify: number of levels, spot sizes, vehicle types, fee structure
- Inheritance: Vehicle and Spot hierarchies with size enum comparison
- Thread safety: lock on park/exit; discuss per-level lock for scale
- Singleton: ParkingLot as singleton; mention stateless alternative for microservices
- Spot selection: first-fit vs best-fit; prefer smallest fitting spot
- Fee: time-based rate per vehicle size; handle edge cases (minimum fee, overstay)
- Extensibility: reservation system, EV charging spots, monthly passes
{“@context”:”https://schema.org”,”@type”:”FAQPage”,”mainEntity”:[{“@type”:”Question”,”name”:”How do you design a parking lot system for an OOP interview?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Core classes: (1) VehicleSize and SpotSize enums with integer values (MOTORCYCLE=1, COMPACT=2, LARGE=3). A vehicle fits in a spot if spot_size.value >= vehicle_size.value — enabling motorcycles to park in compact or large spots. (2) Vehicle ABC with subclasses Motorcycle, Car, Truck — each sets its size in the constructor. can_fit_in(spot_size) centralizes the size-compatibility logic. (3) ParkingSpot stores level, spot_id, size, and the parked vehicle (None if empty). park(vehicle) and remove_vehicle() are the only mutators. (4) Level contains a list of ParkingSpot objects and a find_available_spot(vehicle) method that scans for the first available spot the vehicle can fit in. (5) ParkingLot (Singleton) manages levels, active_tickets dict (license_plate → Ticket), and thread-safe park_vehicle / exit_vehicle methods. (6) Ticket records vehicle, spot, entry_time, and computes fee on exit using hourly rates per vehicle size. The Singleton pattern ensures all application code references the same lot state.”}},{“@type”:”Question”,”name”:”How do you make a parking lot system thread-safe?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”The critical section is the park_vehicle operation: check if vehicle is already parked, scan for an available spot, mark the spot as occupied, create a ticket — all of these steps must be atomic. Without a lock, two threads could both find the same empty spot and both try to park there (TOCTOU race condition). Solution: wrap park_vehicle and exit_vehicle in a threading.Lock (or asyncio.Lock for async frameworks). The lock ensures only one vehicle enters or exits at a time. For higher throughput: use per-level locks. Since vehicles on level 0 and level 1 don’t share spots, their operations don’t need to coordinate. Each Level gets its own lock; ParkingLot acquires the level lock when calling find_available_spot and park. This reduces contention from O(all_vehicles) to O(vehicles_per_level). For a distributed parking lot (multiple buildings, central reservation): use a database with row-level locking (SELECT FOR UPDATE on the spot row) and optimistic concurrency (version column). The application-level lock is replaced by database transactions with retry on conflict.”}},{“@type”:”Question”,”name”:”How do you choose the best parking spot — first-fit vs best-fit?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”First-fit: return the first available spot the vehicle can fit in (scanning spots in order). Simple O(n) scan. Problem: a motorcycle might always be assigned a large spot if large spots appear first in the list — wasting large spots for trucks. Best-fit: return the smallest available spot that fits the vehicle. A motorcycle gets a motorcycle spot if one is available; only uses a compact or large spot when motorcycle spots are full. Implementation: group spots by size in separate lists (dict[SpotSize, deque]). To find a spot for a vehicle of size S: try SpotSize.MOTORCYCLE first if S == MOTORCYCLE, then SpotSize.COMPACT, then SpotSize.LARGE. Pop from the front of the matching deque; push back on exit. This is O(1) per operation instead of O(n) scan. Alternatively, maintain available counts per size and a priority queue of free spot IDs per size. Best-fit maximizes utilization of small spots and reserves large spots for large vehicles. In an interview, propose first-fit first (simpler to code), then mention best-fit as an optimization.”}}]}
🏢 Asked at: Uber Interview Guide 2026: Dispatch Systems, Geospatial Algorithms, and Marketplace Engineering
🏢 Asked at: Lyft Interview Guide 2026: Rideshare Engineering, Real-Time Dispatch, and Safety Systems
🏢 Asked at: Atlassian Interview Guide
🏢 Asked at: Shopify Interview Guide
🏢 Asked at: DoorDash Interview Guide