Skip to content
FollowtheDamnCode

LLD: Parking Lot

Levels, spot sizes, tickets, and fees — the classic LLD warm-up, with spot allocation and pricing as swappable strategies.

Big idea

A parking lot is a set of typed spots, a ticket per stay, and a fee rule. The design's quality shows in two places: which spots a vehicle may use (a small rule that's easy to scatter) and how fees are computed (the thing that always changes). Put both behind clear owners and the rest is bookkeeping.

Requirements

Functional

  • Multiple levels; each has spots of sizes small (motorbike), compact (car), large (van/bus).
  • A vehicle may park in a spot of its size or larger.
  • Entry issues a ticket; exit computes a fee from duration and vehicle type, and frees the spot.
  • Show free spots per level and size.

Out of scope (say so): payments, reservations, multiple entrances racing for the same spot (we'll discuss concurrency at the end).

Entities

ParkingLot, Level, Spot (with SpotSize), Vehicle (with VehicleType), Ticket, FeePolicy (interface), SpotFinder (allocation rule). Time comes from a Clock so tests can control it.

Think of it like

A cloakroom. Each item gets a numbered tag (ticket) and a hook that fits it (spot — coats on hooks, suitcases on the floor). When you hand the tag back, the attendant fetches your item and charges by how long you left it.

Class diagram

Parking lot classes

Key flow: entry and exit

Enter, then exit

Code skeleton

Park, fill up, pay, and free a spot
from enum import IntEnum
import math

class Size(IntEnum):                 # ordered, so "this size or larger" is a comparison
    SMALL = 1
    COMPACT = 2
    LARGE = 3

VEHICLE_SIZE = {"bike": Size.SMALL, "car": Size.COMPACT, "van": Size.LARGE}

class Spot:
    def __init__(self, sid, size):
        self.id, self.size, self.vehicle = sid, size, None
    def is_free(self):
        return self.vehicle is None

class Level:
    def __init__(self, number, sizes):
        self.number = number
        self.spots = [Spot(f"L{number}-{i}", s) for i, s in enumerate(sizes)]
    def free_spot(self, needed):     # smallest free spot that fits: keeps big spots for big vehicles
        fits = [s for s in self.spots if s.is_free() and s.size >= needed]
        return min(fits, key=lambda s: s.size, default=None)

class HourlyFee:                     # a FeePolicy: swap it without touching ParkingLot
    RATES = {"bike": 1, "car": 3, "van": 6}
    def fee(self, ticket, now):
        hours = max(1, math.ceil((now - ticket["at"]) / 60))
        return hours * self.RATES[ticket["type"]]

class ParkingLot:
    def __init__(self, levels, fee_policy, clock):
        self.levels, self.fees, self.clock = levels, fee_policy, clock
        self.active, self.next_id = {}, 1

    def enter(self, plate, vtype):
        needed = VEHICLE_SIZE[vtype]
        for level in self.levels:
            spot = level.free_spot(needed)
            if spot:
                spot.vehicle = plate
                ticket = {"id": self.next_id, "spot": spot, "type": vtype, "at": self.clock()}
                self.active[self.next_id] = ticket
                self.next_id += 1
                return ticket
        return None                  # lot full for this size

    def exit(self, ticket_id):
        ticket = self.active.pop(ticket_id)
        ticket["spot"].vehicle = None
        return self.fees.fee(ticket, self.clock())

    def availability(self):
        return {f"L{l.number}": sum(s.is_free() for s in l.spots) for l in self.levels}


now = [0]                            # a fake clock, in minutes
lot = ParkingLot([Level(0, [Size.SMALL, Size.COMPACT]), Level(1, [Size.COMPACT, Size.LARGE])],
                 HourlyFee(), clock=lambda: now[0])

t1 = lot.enter("BIKE-1", "bike")
t2 = lot.enter("CAR-1", "car")
t3 = lot.enter("CAR-2", "car")
t4 = lot.enter("VAN-1", "van")
print([t["spot"].id for t in (t1, t2, t3, t4)])
print("another van:", lot.enter("VAN-2", "van"))
now[0] = 130                         # 2h10m later
print("car 1 pays", lot.exit(t2["id"]), "| van pays", lot.exit(t4["id"]))
print("free:", lot.availability())
output
['L0-0', 'L0-1', 'L1-0', 'L1-1']
another van: None
car 1 pays 9 | van pays 18
free: {'L0': 1, 'L1': 1}

Run and edit it in the app.

Notice the allocation rule — "smallest spot that fits" — lives in one method. Change it to "nearest the exit" and nothing else moves.

Watch it

The business adds weekend flat rates and a first-30-minutes-free promotion. Where should that change go?

Two entry gates call enter() at the same moment and both pick the last free compact spot. What's the simplest fix?

Extensions and trade-offs

  • New vehicle types (electric cars needing chargers): add a spot feature, not a new size; the finder filters by features.
  • Display boards: Observer — levels publish "spot freed/taken" events.
  • Reservations: a spot gains a "held until" state — see Movie ticket booking for the hold pattern.
  • Concurrency: atomic claim per spot; at scale, partition by level.
  • Persistence: tickets and spot state in a database; the classes stay the same behind a repository (Dependency Inversion).

Key terms

  • Spot allocation — The rule for choosing which free spot a vehicle gets.
  • Fee policy — The swappable pricing rule applied at exit.
  • Atomic claim — Finding and taking a resource in one indivisible step so two requests can't both win.