Design a Parking Lot: The Classic LLD Interview Problem, Reasoned Through
The parking lot problem is one of those interview questions that sounds almost embarrassingly simple. You have spots, you have cars, you assign cars to spots. It takes maybe ten minutes to sketch the “obvious” version, and then the interviewer starts adding requirements and you realize you built yourself into a corner.
The trap is jumping straight to implementation before thinking about the entities. If you model ParkingSpot as a class with subclasses CompactSpot, LargeSpot, and HandicappedSpot, you now have three places to add logic whenever the rules change. If you model spot type as an enum on a single ParkingSpot class, you have one. That choice sounds small, but it ripples through everything downstream: how you search for available spots, how you match vehicles to spots, how you calculate fees.
Let me reason through the whole design from scratch, making the trade-offs explicit at each step.
Requirements
Functional:
- Multiple floors, each with multiple spots of varying types (compact, large, handicapped)
- Vehicle types: motorcycle, car, truck. Spot eligibility depends on the pairing (e.g., cars only fit compact and large)
- Issue a ticket when a vehicle enters, close the ticket on exit
- Calculate fees on exit using a configurable pricing strategy (hourly, flat rate, premium)
- Display boards on each floor show available spot counts per type in real time
Non-functional:
- The system must handle concurrent entries without double-assigning a spot
- Adding a new pricing model should not require touching the ticket or spot classes
- The design must extend to hundreds of floors without structural changes
Core Entities
ParkingSpot owns a location (floor, spot ID), a type (enum), and a boolean for occupancy. It does not own vehicle logic. It does not calculate fees. It knows where it is and whether it is taken.
Vehicle carries a license plate and a vehicle type (also an enum). It does not know which spot it fits into. That matching logic belongs in a dedicated place so the rules can change without touching Vehicle.
SpotMatcher is a small helper that given a vehicle type returns the set of spot types it is allowed to occupy. Keeping this separate means the eligibility table lives in exactly one place.
ParkingFloor owns a collection of spots and answers one key question: given a vehicle type, find the nearest available spot. It also notifies the display board when occupancy changes.
ParkingLot is the top-level orchestrator. It owns the floors, the active tickets, and the pricing strategy. It exposes enter() and exit() and nothing else to external callers.
Ticket is a value object capturing the entry moment, the vehicle, and the assigned spot. On exit it gets stamped with the exit time, and the pricing strategy uses both timestamps to compute the fee.
PricingStrategy is an abstract base that takes a Ticket and returns a fee in dollars. Concrete implementations handle hourly, flat, and premium pricing.
DisplayBoard subscribes to occupancy events per floor and keeps a current count of free spots by type. This is the Observer pattern at work: the floor fires an event, the board updates itself, no tight coupling required.
ASCII Class Diagram
+--------------------+ +----------------------+
| ParkingLot |---->| PricingStrategy |
|--------------------| |----------------------|
| - floors | | + calculate(ticket) |
| - tickets | +----------+-----------+
| - strategy | |
| + enter(vehicle) | HourlyFee FlatFee PremiumFee
| + exit(ticket_id) |
+--------------------+
|
| owns
v
+--------------------+ +-------------------+
| ParkingFloor |---->| DisplayBoard |
|--------------------| |-------------------|
| - floor_num | | + on_change(type, |
| - spots | | delta: int) |
| + find_spot(vtype) | +-------------------+
| - _notify(...) |
+--------------------+
|
| owns
v
+--------------------+ +-------------------+
| ParkingSpot | | Vehicle |
|--------------------| |-------------------|
| - spot_id | | - license_plate |
| - spot_type | | - vehicle_type |
| - is_occupied | +-------------------+
| + occupy() |
| + vacate() |
+--------------------+
^
|
+--------------------+
| Ticket |
|--------------------|
| - ticket_id |
| - vehicle |
| - spot |
| - entry_time |
| - exit_time |
+--------------------+
Key Implementation
from __future__ import annotations
import threading
import uuid
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum, auto
from typing import Optional
class SpotType(Enum):
COMPACT = auto()
LARGE = auto()
HANDICAPPED = auto()
class VehicleType(Enum):
MOTORCYCLE = auto()
CAR = auto()
TRUCK = auto()
# Eligibility table: which spot types each vehicle type can use.
# Changing a rule means changing this dict and nothing else.
ELIGIBLE_SPOTS: dict[VehicleType, list[SpotType]] = {
VehicleType.MOTORCYCLE: [SpotType.COMPACT, SpotType.LARGE],
VehicleType.CAR: [SpotType.COMPACT, SpotType.LARGE],
VehicleType.TRUCK: [SpotType.LARGE],
}
@dataclass
class Vehicle:
license_plate: str
vehicle_type: VehicleType
@dataclass
class ParkingSpot:
spot_id: str
floor_num: int
spot_type: SpotType
is_occupied: bool = False
def occupy(self) -> None:
if self.is_occupied:
raise ValueError(f"Spot {self.spot_id} is already occupied.")
self.is_occupied = True
def vacate(self) -> None:
self.is_occupied = False
@dataclass
class Ticket:
ticket_id: str
vehicle: Vehicle
spot: ParkingSpot
entry_time: datetime
exit_time: Optional[datetime] = None
def close(self) -> None:
self.exit_time = datetime.now()
def duration_hours(self) -> float:
if self.exit_time is None:
raise ValueError("Ticket is not closed yet.")
delta = self.exit_time - self.entry_time
return delta.total_seconds() / 3600
class PricingStrategy(ABC):
@abstractmethod
def calculate(self, ticket: Ticket) -> float:
pass
class HourlyFee(PricingStrategy):
def __init__(self, rate_per_hour: float) -> None:
self._rate = rate_per_hour
def calculate(self, ticket: Ticket) -> float:
# Ceil to the nearest hour so a 61-minute stay costs 2 hours.
import math
return math.ceil(ticket.duration_hours()) * self._rate
class FlatFee(PricingStrategy):
def __init__(self, flat_rate: float) -> None:
self._rate = flat_rate
def calculate(self, ticket: Ticket) -> float:
return self._rate
class PremiumFee(PricingStrategy):
"""Higher rate for the first hour, standard hourly after that."""
def __init__(self, premium_first_hour: float, rate_per_hour: float) -> None:
self._premium = premium_first_hour
self._standard = rate_per_hour
def calculate(self, ticket: Ticket) -> float:
import math
hours = ticket.duration_hours()
if hours <= 1:
return self._premium
return self._premium + math.ceil(hours - 1) * self._standard
class DisplayBoard:
def __init__(self, floor_num: int) -> None:
self._floor_num = floor_num
self._counts: dict[SpotType, int] = {t: 0 for t in SpotType}
def set_count(self, spot_type: SpotType, count: int) -> None:
self._counts[spot_type] = count
def on_change(self, spot_type: SpotType, delta: int) -> None:
self._counts[spot_type] = max(0, self._counts[spot_type] + delta)
def available(self, spot_type: SpotType) -> int:
return self._counts[spot_type]
class ParkingFloor:
def __init__(self, floor_num: int, spots: list[ParkingSpot]) -> None:
self._floor_num = floor_num
self._spots = spots
self._lock = threading.Lock()
self._board = DisplayBoard(floor_num)
self._init_board()
def _init_board(self) -> None:
for spot_type in SpotType:
count = sum(
1 for s in self._spots
if s.spot_type == spot_type and not s.is_occupied
)
self._board.set_count(spot_type, count)
def find_and_assign(self, vehicle_type: VehicleType) -> Optional[ParkingSpot]:
"""
Finds the nearest free spot for the vehicle type.
Uses a lock to prevent two threads from claiming the same spot.
Spots are stored in order, so the first match is the nearest.
"""
eligible = ELIGIBLE_SPOTS[vehicle_type]
with self._lock:
for spot in self._spots:
if not spot.is_occupied and spot.spot_type in eligible:
spot.occupy()
self._board.on_change(spot.spot_type, -1)
return spot
return None
def release(self, spot: ParkingSpot) -> None:
with self._lock:
spot.vacate()
self._board.on_change(spot.spot_type, +1)
def available_count(self, spot_type: SpotType) -> int:
return self._board.available(spot_type)
class ParkingLot:
def __init__(
self,
floors: list[ParkingFloor],
strategy: PricingStrategy,
) -> None:
self._floors = floors
self._strategy = strategy
self._tickets: dict[str, Ticket] = {}
def enter(self, vehicle: Vehicle) -> Ticket:
for floor in self._floors:
spot = floor.find_and_assign(vehicle.vehicle_type)
if spot:
ticket = Ticket(
ticket_id=str(uuid.uuid4()),
vehicle=vehicle,
spot=spot,
entry_time=datetime.now(),
)
self._tickets[ticket.ticket_id] = ticket
return ticket
raise RuntimeError("Parking lot is full.")
def exit(self, ticket_id: str) -> float:
ticket = self._tickets.get(ticket_id)
if ticket is None:
raise ValueError(f"Unknown ticket: {ticket_id}")
ticket.close()
fee = self._strategy.calculate(ticket)
floor = self._floors[ticket.spot.floor_num]
floor.release(ticket.spot)
del self._tickets[ticket_id]
return fee
Design Decisions and Trade-offs
Enum for spot type vs. inheritance.
The inheritance path is tempting: CompactSpot(ParkingSpot), LargeSpot(ParkingSpot), each with its own behavior. The problem is that spot behavior is entirely uniform. All spots occupy and vacate the same way. The only thing that varies is the type label and the eligibility rules. Subclassing to hold a type label is the classic over-engineering trap. An enum captures the variation cleanly, and the ELIGIBLE_SPOTS dict captures the rules in one place. If you later need spot-type-specific behavior (say, handicapped spots require a permit check), you can add a method to ParkingSpot that branches on self.spot_type without restructuring anything.
Observer for the display board.
The floor does not reach into the display board and update counts directly in a procedural way. Instead, it notifies the board through on_change() with a signed delta. This means the board can be replaced, extended, or mocked in tests without touching the floor. A future version could push these updates to a WebSocket broadcast or a digital signage API by swapping the board implementation, and ParkingFloor would not notice.
Concurrent spot allocation.
The tricky part is two cars arriving simultaneously and being assigned to the same spot. The lock in find_and_assign is a coarse-grained mutex on the floor. While a car is being assigned, no other car on that floor can be processed. This is the pessimistic approach and it is correct. The optimistic alternative (check then compare-and-swap) requires a CAS primitive on the spot’s occupied flag, which Python does not expose cleanly. For a parking lot with dozens of cars per minute, the pessimistic lock is fast enough and easier to reason about.
Finding the nearest spot.
The implementation above treats “nearest” as the first spot in a pre-ordered list. The real definition of nearest depends on the layout: in a single-floor lot it might be Euclidean distance from the entrance. In a multi-floor lot it might factor in elevator proximity. By keeping the search inside ParkingFloor.find_and_assign(), you can replace the linear scan with a priority queue ordered by distance without changing anything outside the class.
Pricing strategy and the Strategy pattern.
Adding a new pricing model (say, weekend rates or validation-based free parking) means adding a new class that implements PricingStrategy.calculate(). ParkingLot does not change. Ticket does not change. The Strategy pattern costs one extra level of indirection and buys you completely isolated, independently testable pricing logic.
What I’d Extend Next
If I were extending this further, I’d add a PaymentProcessor that takes a fee and a payment method, keeping payment logic out of ParkingLot. I’d also make Ticket immutable after closing by raising on any mutation after exit_time is set. For analytics, a ParkingEvent log that records every entry, exit, and spot assignment lets you reconstruct occupancy history without querying live state.
If any of this sparked a question or you’d approach the concurrency problem differently, I’d genuinely like to hear it. Reach out on Twitter or LinkedIn.
Tags:
Related Posts
Design a Vending Machine: State Machines in Practice
A first-principles LLD walkthrough of a vending machine using the State pattern. Covers state transitions, inventory management, payment handling, and why State beats if/else chains when behavior must vary by state.
Design an ATM Machine: Transactions, State, and Security in LLD
A first-principles LLD walkthrough of an ATM covering state management, transaction types, card and account entities, cash dispensing logic, and why atomicity matters even in a machine-coding exercise.
Design a Logging Library Like log4j: Filters, Appenders, and Formatters
A first-principles LLD walkthrough of a logging library covering logger hierarchy, log levels, appenders, formatters, and the Chain of Responsibility pattern for filter chains. Why appenders must be independent of formatters.