Courses › Low-Level Design › Problem Walkthroughs

Design a Parking Lot

Entities, spot allocation strategies, tickets and pricing.

"Design a parking lot" is the most classic LLD question. It's simple to understand, which leaves room to show good modeling, a clean use of the Strategy pattern, and attention to concurrency.

Requirements

Clarify first. A reasonable scope:

  • The lot has multiple levels, each with spots of different sizes: small (motorcycle), medium (car), large (truck/van)
  • A vehicle enters: the system finds a suitable free spot and issues a ticket
  • A vehicle exits: the system computes the fee based on duration, frees the spot
  • Show availability per level

Out of scope: reservations, payment processing, multiple entrances (mention as extensions).

Entities

Fitting rules

A vehicle fits in a spot of its size or larger. Encode it on the enum's ordering so the rule lives in one place.

Implementation

from dataclasses import dataclass, field
from datetime import datetime
from enum import IntEnum
from threading import Lock
import itertools, math


class VehicleSize(IntEnum):
    SMALL = 1
    MEDIUM = 2
    LARGE = 3


@dataclass(frozen=True)
class Vehicle:
    plate: str
    size: VehicleSize


class ParkingSpot:
    def __init__(self, spot_id: str, size: VehicleSize):
        self.id = spot_id
        self.size = size
        self.vehicle = None

    def can_fit(self, vehicle: Vehicle) -> bool:
        return self.vehicle is None and vehicle.size <= self.size

    def assign(self, vehicle: Vehicle):
        self.vehicle = vehicle

    def release(self):
        self.vehicle = None


class Level:
    def __init__(self, number: int, spots: list[ParkingSpot]):
        self.number = number
        self.spots = spots

    def free_spots(self, vehicle: Vehicle):
        return [s for s in self.spots if s.can_fit(vehicle)]


@dataclass
class Ticket:
    id: int
    vehicle: Vehicle
    spot: ParkingSpot
    entry_time: datetime
    exit_time: datetime | None = None


class AllocationStrategy:
    def choose(self, levels: list[Level], vehicle: Vehicle) -> ParkingSpot | None:
        raise NotImplementedError


class SmallestFitLowestLevel(AllocationStrategy):
    """Prefer the lowest level, and the smallest spot that fits, to keep big spots free."""
    def choose(self, levels, vehicle):
        for level in levels:
            spots = level.free_spots(vehicle)
            if spots:
                return min(spots, key=lambda s: s.size)
        return None


class PricingStrategy:
    def fee_cents(self, ticket: Ticket) -> int:
        raise NotImplementedError


class HourlyPricing(PricingStrategy):
    RATES = {VehicleSize.SMALL: 200, VehicleSize.MEDIUM: 400, VehicleSize.LARGE: 800}

    def fee_cents(self, ticket):
        minutes = (ticket.exit_time - ticket.entry_time).total_seconds() / 60
        hours = max(1, math.ceil(minutes / 60))
        return hours * self.RATES[ticket.vehicle.size]


class LotFullError(Exception):
    pass


class ParkingLot:
    def __init__(self, levels, allocation: AllocationStrategy, pricing: PricingStrategy, clock=datetime.now):
        self.levels = levels
        self.allocation = allocation
        self.pricing = pricing
        self.clock = clock
        self.active = {}                    # ticket id -> Ticket
        self._ids = itertools.count(1)
        self._lock = Lock()                 # spot assignment must be atomic

    def enter(self, vehicle: Vehicle) -> Ticket:
        with self._lock:
            spot = self.allocation.choose(self.levels, vehicle)
            if spot is None:
                raise LotFullError(f"No spot for {vehicle.size.name}")
            spot.assign(vehicle)
            ticket = Ticket(next(self._ids), vehicle, spot, self.clock())
            self.active[ticket.id] = ticket
            return ticket

    def exit(self, ticket_id: int) -> int:
        with self._lock:
            ticket = self.active.pop(ticket_id)
            ticket.exit_time = self.clock()
            ticket.spot.release()
        return self.pricing.fee_cents(ticket)

Design decisions to explain

  • Strategies for allocation and pricing — both are likely to change (EV spots, weekend rates), so they're injected interfaces. *Open/closed* and *dependency inversion*.
  • ParkingLot as a facade — callers have two operations; internals can evolve.
  • Injected clock — makes pricing testable.
  • Locking — without the lock, two threads could choose the same free spot. A single lock is simple and fine for one lot; for higher throughput, lock per level or use atomic compare-and-set on each spot.

Extensions to discuss

  • Multiple entrances — each entrance runs enter concurrently; the lock (or per-spot atomic assignment) already handles it.
  • EV charging spots — add a spot feature set and a strategy that prefers charging spots for EVs.
  • Availability display — an Observer that updates level counters on assign/release, instead of recounting.
  • Reservations — a Reservation entity that holds a spot for a time window.
  • Persistence — tickets stored in a repository interface so the lot survives restarts.