Low-Level Design: Design a Parking Lot System
Parking lot is a classic Low-Level Design (LLD) / Object-Oriented Design interview question at Uber, Lyft, Amazon, and Microsoft. It tests your ability to model a real-world system with classes, inheritance, and design patterns.
Requirements Clarification
Functional Requirements
- Multiple floors, each with multiple spots of different sizes (compact, regular, large, motorcycle)
- Park a vehicle and get a ticket
- Unpark a vehicle, calculate fee, and process payment
- Display available spots per floor per type
- Support different vehicle types (motorcycle, car, bus/truck)
Non-Functional
- Thread-safe: multiple entrance/exit lanes operating concurrently
- Extensible: easy to add new vehicle types or pricing strategies
Class Design
from enum import Enum
from datetime import datetime
import threading
class VehicleType(Enum):
MOTORCYCLE = 1
CAR = 2
BUS = 3
class SpotSize(Enum):
SMALL = 1 # motorcycles only
MEDIUM = 2 # motorcycles and cars
LARGE = 3 # all vehicles
class Vehicle:
def __init__(self, plate: str, vehicle_type: VehicleType):
self.plate = plate
self.vehicle_type = vehicle_type
self.spot = None # assigned spot
def can_fit_in(self, spot_size: SpotSize) -> bool:
if self.vehicle_type == VehicleType.MOTORCYCLE:
return True # fits in any spot
if self.vehicle_type == VehicleType.CAR:
return spot_size in (SpotSize.MEDIUM, SpotSize.LARGE)
if self.vehicle_type == VehicleType.BUS:
return spot_size == SpotSize.LARGE
return False
class ParkingSpot:
def __init__(self, spot_id: str, floor: int, size: SpotSize):
self.spot_id = spot_id
self.floor = floor
self.size = size
self.is_occupied = False
self.vehicle = None
self._lock = threading.Lock()
def park(self, vehicle: Vehicle) -> bool:
with self._lock:
if self.is_occupied or not vehicle.can_fit_in(self.size):
return False
self.is_occupied = True
self.vehicle = vehicle
vehicle.spot = self
return True
def unpark(self) -> Vehicle:
with self._lock:
vehicle = self.vehicle
self.is_occupied = False
self.vehicle = None
if vehicle:
vehicle.spot = None
return vehicle
class Ticket:
def __init__(self, ticket_id: str, vehicle: Vehicle, spot: ParkingSpot):
self.ticket_id = ticket_id
self.vehicle = vehicle
self.spot = spot
self.entry_time = datetime.now()
self.exit_time = None
self.fee = 0.0
Pricing Strategy (Strategy Pattern)
from abc import ABC, abstractmethod
class PricingStrategy(ABC):
@abstractmethod
def calculate_fee(self, entry_time: datetime, exit_time: datetime,
vehicle_type: VehicleType) -> float:
pass
class HourlyPricing(PricingStrategy):
RATES = {
VehicleType.MOTORCYCLE: 1.0,
VehicleType.CAR: 2.0,
VehicleType.BUS: 5.0,
}
def calculate_fee(self, entry_time, exit_time, vehicle_type):
duration = (exit_time - entry_time).total_seconds() / 3600
hours = max(1, int(duration) + (1 if duration % 1 else 0)) # ceil
return hours * self.RATES[vehicle_type]
class FlatRatePricing(PricingStrategy):
def calculate_fee(self, entry_time, exit_time, vehicle_type):
return 10.0 # flat rate regardless of time
ParkingFloor and ParkingLot
class ParkingFloor:
def __init__(self, floor_number: int, spots: list):
self.floor_number = floor_number
self.spots = spots # list of ParkingSpot
self._available = {size: [] for size in SpotSize}
for spot in spots:
self._available[spot.size].append(spot)
def find_spot(self, vehicle: Vehicle):
# Find smallest fitting spot (motorcycle prefers small spots)
preferred_size = {
VehicleType.MOTORCYCLE: SpotSize.SMALL,
VehicleType.CAR: SpotSize.MEDIUM,
VehicleType.BUS: SpotSize.LARGE,
}[vehicle.vehicle_type]
for size in [preferred_size] + list(SpotSize):
for spot in self._available.get(size, []):
if not spot.is_occupied and vehicle.can_fit_in(size):
return spot
return None
def available_count(self, size: SpotSize) -> int:
return sum(1 for s in self._available.get(size, []) if not s.is_occupied)
import uuid
class ParkingLot:
def __init__(self, floors: list, pricing: PricingStrategy):
self.floors = floors
self.pricing = pricing
self.tickets = {} # ticket_id -> Ticket
self._lock = threading.Lock()
def park(self, vehicle: Vehicle):
for floor in self.floors:
spot = floor.find_spot(vehicle)
if spot and spot.park(vehicle):
ticket_id = str(uuid.uuid4())[:8]
ticket = Ticket(ticket_id, vehicle, spot)
with self._lock:
self.tickets[ticket_id] = ticket
print(f"Parked {vehicle.plate} at floor {spot.floor} spot {spot.spot_id}")
return ticket
raise Exception("Parking lot is full")
def unpark(self, ticket_id: str) -> float:
with self._lock:
ticket = self.tickets.pop(ticket_id, None)
if not ticket:
raise Exception("Invalid ticket")
ticket.exit_time = datetime.now()
ticket.spot.unpark()
ticket.fee = self.pricing.calculate_fee(
ticket.entry_time, ticket.exit_time, ticket.vehicle.vehicle_type)
print("Fee for %s: $%.2f" % (ticket.vehicle.plate, ticket.fee))
return ticket.fee
def display_availability(self):
for floor in self.floors:
print(f"Floor {floor.floor_number}:")
for size in SpotSize:
print(f" {size.name}: {floor.available_count(size)} available")
Key Design Decisions
- Strategy pattern for pricing: easy to swap HourlyPricing for FlatRatePricing or add DynamicPricing
- Thread safety: per-spot lock for parking operations, global lock for ticket registry
- Best-fit allocation: motorcycles prefer small spots, leaving medium spots for cars
- Extensibility: new vehicle types just implement can_fit_in; new pricing just implements calculate_fee
Interview Tips
- Start by asking clarifying questions about vehicle types and pricing models
- Draw the class diagram before coding: Vehicle, ParkingSpot, ParkingFloor, ParkingLot, Ticket, PricingStrategy
- Mention thread safety – multiple entry/exit lanes need concurrent access
- Name the design patterns you use (Strategy for pricing, Lock for thread safety)
- Discuss the best-fit vs first-fit spot allocation trade-off