~learn.

LLD 0112 min+60 XP

Design a Parking Lot.

Design a multi-floor parking lot, then make it run

5 code

Lesson · 12 min

The most-asked machine-coding problem in India, and the one where most candidates lose marks for things that have nothing to do with parking.

The parking lot is the "hello world" of the machine-coding rounds that Indian product companies such as Flipkart, Swiggy, PhonePe and Razorpay are known for. It looks easy, and that is the trap. Everyone can write a ParkingLot class. The interviewer wants to see whether you separate the parts that change (pricing, spot selection) from the parts that don't, whether your code runs, and whether you notice that two gates hitting park() at the same moment is a race.

This lesson runs the round the way an interviewer would, end to end: the clarifying questions, the classes, the two interfaces, the one lock that matters, about 150 lines of Python you can run, and the follow-ups they ask in the last fifteen minutes.

ExplainerThe whole design: Parking lot

A 90-minute machine-coding round: clarify what fits where, which spot to give and how to charge, then model ParkingLot as the only stateful class with two injected strategies (spot assignment and pricing) over plain data. A car enters, the strategy picks the lowest floor and smallest fitting spot under one lock, a ticket is issued, and at exit 2 h 5 min is billed as 3 hours, ₹120. Without the lock two gates can park in one spot; the demo races 40 cars for 6 spots.

How the round runs#

Interviewer

"Design a parking lot. Multiple floors, different vehicle sizes, tickets at entry, payment at exit. You have 90 minutes. I want working code at the end, with a small demo in main. Ask me anything."

That "ask me anything" is graded. A machine-coding round is not a system design round: nobody cares about load balancers here. It is scored on four things, roughly equally:

What the interviewer is scoring
  • Requirements: did you pin the scope down in 5–10 minutes, or start coding a guess?
  • Modelling: are the classes the nouns of the problem, with one job each? Are the things that vary behind interfaces?
  • Working code: does it run, does the demo prove the rules, is it readable without you narrating it?
  • Extensibility: when they add a requirement at minute 75, how many existing lines do you edit?
A good 90 minutes
  • 0–10 min —
    clarify: vehicle types, spot sizes, pricing, how many gates, what "full" means
  • 10–25 min —
    entities and interfaces on paper (or as empty classes), agreed with the interviewer
  • 25–70 min —
    code it, bottom-up: enums and data classes, the two strategies, then the lot
  • 70–80 min —
    a main that parks, rejects, charges and races two gates
  • 80–90 min —
    follow-ups: a new vehicle type, an EV spot, a display board, per-floor locking

Requirements and clarifying questions#

Ask questions whose answers change the code. "How many floors?" does not (it's a number); "can a bike park in a car spot?" does.

You ask They answer What it changes
Which vehicle types? Bike, car, truck. A VehicleType enum
Which spot sizes, and what fits where? Small, medium, large. A vehicle can take its own size or larger. A fits() rule, not a lookup per pair
Which spot do we give? Nearest to the entrance: lowest floor first. A SpotAssignmentStrategy
How is the fee computed? Hourly by vehicle type, every started hour billed, minimum one hour. A PricingStrategy
How many entry and exit gates? Several, working at the same time. A lock: concurrency is in scope
Payment modes, lost tickets, reservations? Out of scope; mention them at the end. Nothing now; a follow-up later
Scope we agreed

In scope

  • Floors with small, medium and large spots
  • Park a vehicle: assign a spot, issue a ticket
  • Leave: free the spot, compute the fee
  • Show free spots per floor and size
  • Several gates calling at once

Out of scope (for now)

  • Payment processing (cash, UPI, FASTag)
  • Reservations and monthly passes
  • Lost tickets and penalties
  • Persistence; everything lives in memory

Wait for the interviewer to agree before you draw a single class. If they add "EV charging spots" now, you'll design for it; if they add it at minute 80, you'll want a design that absorbs it. Both are fine, as long as you asked.

Your turn · checkpoint 1not answered

Before the classes: which parts will change?

Look at the requirements. Which two behaviours are most likely to change after launch, and so belong behind an interface?

Think it through, or jot it down. Then compare.

One strong answer

Which spot to give (nearest first today; tomorrow "keep the ground floor for EVs" or "fill one floor at a time so the others can be closed at night") and what to charge (hourly today; tomorrow weekend rates, a flat night fee, or a free first 30 minutes for a mall). Both become strategies the lot is given in its constructor. Everything else (a spot, a ticket, a vehicle) is plain data.

The classes#

Pull the nouns out of the problem: lot, floor, spot, vehicle, ticket, receipt. Pull the verbs out too: park, leave, price, find a spot. Nouns become classes; verbs that vary become interfaces.

DiagramParking lot: the classes
%% title: Parking lot: the classes
classDiagram
  class ParkingLot {
    +park(vehicle, at) Ticket
    +leave(ticket_id, at) Receipt
    +availability() dict
  }
  class Floor {
    +level int
    +free_count(size) int
  }
  class ParkingSpot {
    +spot_id str
    +size SpotSize
    +vehicle Vehicle
    +fits(vehicle) bool
  }
  class Vehicle {
    +plate str
    +type VehicleType
  }
  class Ticket {
    +ticket_id str
    +spot_id str
    +entry_at datetime
  }
  class SpotAssignmentStrategy {
    <<interface>>
    +find_spot(floors, vehicle) ParkingSpot
  }
  class PricingStrategy {
    <<interface>>
    +price(ticket, exit_at) Receipt
  }
  ParkingLot "1" *-- "many" Floor
  Floor "1" *-- "many" ParkingSpot
  ParkingSpot --> Vehicle
  Ticket --> Vehicle
  ParkingLot --> SpotAssignmentStrategy
  ParkingLot --> PricingStrategy
  SpotAssignmentStrategy <|.. NearestSmallestFit
  PricingStrategy <|.. HourlyPricing

Three decisions are worth saying out loud while you draw this:

  • ParkingLot is the only class that changes state. Spots, floors and tickets are data. That gives you exactly one place to put a lock, and one class to test.
  • Vehicles don't get subclasses. Car(Vehicle) and Bike(Vehicle) with no behaviour of their own are a classic over-design. A type enum is enough until a vehicle actually behaves differently.
  • The ticket stores the spot id, not the spot. A ticket is a record of what happened; it should not keep a live object alive or let someone mutate a spot through it.

The interfaces#

Start with the vocabulary. Frozen dataclasses for things that never change once created (a vehicle, a ticket, a receipt); a mutable one for the spot, whose vehicle changes. Money is an int in rupees; never a float.

pythonVehicleType · 70 lines
# parking_lot.py, part 1: the vocabulary (enums and plain data)
from __future__ import annotations

import itertools
import math
import threading
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from datetime import datetime, timedelta
from enum import Enum


class VehicleType(Enum):
    BIKE = "bike"
    CAR = "car"
    TRUCK = "truck"


class SpotSize(Enum):
    SMALL = 1
    MEDIUM = 2
    LARGE = 3


# the smallest spot each vehicle fits in; anything larger also fits
MIN_SIZE = {VehicleType.BIKE: SpotSize.SMALL, VehicleType.CAR: SpotSize.MEDIUM, VehicleType.TRUCK: SpotSize.LARGE}


@dataclass(frozen=True)
class Vehicle:
    plate: str
    type: VehicleType


@dataclass
class ParkingSpot:
    spot_id: str
    floor: int
    size: SpotSize
    vehicle: Vehicle | None = None

    def is_free(self) -> bool:
        return self.vehicle is None

    def fits(self, v: Vehicle) -> bool:
        return self.size.value >= MIN_SIZE[v.type].value


@dataclass
class Floor:
    level: int
    spots: list[ParkingSpot] = field(default_factory=list)

    def free_count(self, size: SpotSize) -> int:
        return sum(1 for s in self.spots if s.size is size and s.is_free())


@dataclass(frozen=True)
class Ticket:
    ticket_id: str
    vehicle: Vehicle
    spot_id: str
    entry_at: datetime


@dataclass(frozen=True)
class Receipt:
    ticket_id: str
    hours: int
    amount: int  # rupees

Now the two seams. Each is one method, so each implementation is small and easy to test on its own. NearestSmallestFit gives the lowest floor first and, on that floor, the smallest spot that fits, so a bike doesn't burn the only large spot a truck could use.

pythonSpotAssignmentStrategy · 32 lines
# part 2: the two interfaces, and one implementation of each
class SpotAssignmentStrategy(ABC):
    @abstractmethod
    def find_spot(self, floors: list[Floor], vehicle: Vehicle) -> ParkingSpot | None: ...


class NearestSmallestFit(SpotAssignmentStrategy):
    """Lowest floor first; on a floor, the smallest spot that fits (keep large spots for trucks)."""

    def find_spot(self, floors: list[Floor], vehicle: Vehicle) -> ParkingSpot | None:
        for floor in sorted(floors, key=lambda f: f.level):
            fitting = [s for s in floor.spots if s.is_free() and s.fits(vehicle)]
            if fitting:
                return min(fitting, key=lambda s: (s.size.value, s.spot_id))
        return None


class PricingStrategy(ABC):
    @abstractmethod
    def price(self, ticket: Ticket, exit_at: datetime) -> Receipt: ...


class HourlyPricing(PricingStrategy):
    """Every started hour is billed; the minimum is one hour."""

    def __init__(self, rates: dict[VehicleType, int]) -> None:
        self.rates = rates

    def price(self, ticket: Ticket, exit_at: datetime) -> Receipt:
        seconds = max(0.0, (exit_at - ticket.entry_at).total_seconds())
        hours = max(1, math.ceil(seconds / 3600))
        return Receipt(ticket.ticket_id, hours, hours * self.rates[ticket.vehicle.type])

The patterns, and why#

Pattern Where Why it earns its place
Strategy SpotAssignmentStrategy, PricingStrategy The two behaviours the business will change. A weekend tariff is a new class passed to the constructor; ParkingLot is not edited (open/closed).
Dependency injection ParkingLot(floors, assigner, pricing) Tests pass a fixed strategy and a fixed time. Nothing reaches for a global.
Factory function build_lot() Building floors and spots is noise; keep it out of the class that runs the lot.
Singleton (avoided) — Many candidates make ParkingLot a singleton. It buys nothing and makes tests share state. Say you'd create one instance at startup and pass it around.

Concurrency and edge cases#

Two entry gates run on two threads. Both call park(). Both call find_spot(), both see F0-M0 free, both write their car into it. Two tickets, one spot. The fix is to make find and occupy one atomic step: one lock around both. Pricing touches no shared state, so it runs outside the lock.

Edge cases to handle, and say you handled
  • Lot full for this vehicle type: raise a specific LotFullError, not a None the caller forgets to check
  • Same plate twice: a car that is already inside can't park again (a cloned number plate, or a gate glitch)
  • Ticket used twice or unknown: reject; the second exit attempt must not free someone else's spot
  • Exit time before entry time (clock skew between gate machines): clamp to zero, still bill the minimum hour
  • A bike in the only free car spot: allowed by the rules, but the strategy prefers the smallest fit first
  • Time is a parameter (at), never datetime.now() inside the class: the demo and the tests control the clock

One lock for the whole lot is correct and simple. If the interviewer pushes on throughput (a 5,000-spot mall, 20 gates), the answer is a lock per floor, and a gate tries floors in order; but a parking lot sees a few cars a second, so start simple and say why.

The code#

The lot itself. Note what is inside the lock and what isn't:

pythonLotFullError · 40 lines
# part 3: the lot itself, the only class that changes state
class LotFullError(Exception): ...


class ParkingLot:
    def __init__(self, floors: list[Floor], assigner: SpotAssignmentStrategy, pricing: PricingStrategy) -> None:
        self.floors = floors
        self.assigner = assigner
        self.pricing = pricing
        self._spots = {s.spot_id: s for f in floors for s in f.spots}
        self._active: dict[str, Ticket] = {}  # ticket_id -> ticket
        self._parked: set[str] = set()  # plates inside right now
        self._ids = itertools.count(1)
        self._lock = threading.Lock()  # entry and exit gates call in on different threads

    def park(self, vehicle: Vehicle, at: datetime) -> Ticket:
        with self._lock:  # find + occupy must be one atomic step
            if vehicle.plate in self._parked:
                raise ValueError(f"{vehicle.plate} is already inside")
            spot = self.assigner.find_spot(self.floors, vehicle)
            if spot is None:
                raise LotFullError(f"no spot for a {vehicle.type.value}")
            spot.vehicle = vehicle
            ticket = Ticket(f"T{next(self._ids):04d}", vehicle, spot.spot_id, at)
            self._active[ticket.ticket_id] = ticket
            self._parked.add(vehicle.plate)
            return ticket

    def leave(self, ticket_id: str, at: datetime) -> Receipt:
        with self._lock:
            ticket = self._active.pop(ticket_id, None)
            if ticket is None:
                raise KeyError(f"unknown or already used ticket {ticket_id}")
            self._spots[ticket.spot_id].vehicle = None
            self._parked.discard(ticket.vehicle.plate)
        return self.pricing.price(ticket, at)  # pure, so it can run outside the lock

    def availability(self) -> dict[int, dict[str, int]]:
        with self._lock:
            return {f.level: {z.name: f.free_count(z) for z in SpotSize} for f in self.floors}

And the demo: park three vehicles, check availability, charge a 2-hour-5-minute stay, reject a reused ticket, then race 40 cars through the gates at once.

pythonbuild_lot · 42 lines
# part 4: build a small lot and drive it
def build_lot(levels: int, layout: dict[SpotSize, int]) -> ParkingLot:
    floors = []
    for lv in range(levels):
        spots = [ParkingSpot(f"F{lv}-{size.name[0]}{i}", lv, size) for size, n in layout.items() for i in range(n)]
        floors.append(Floor(lv, spots))
    rates = {VehicleType.BIKE: 20, VehicleType.CAR: 40, VehicleType.TRUCK: 100}
    return ParkingLot(floors, NearestSmallestFit(), HourlyPricing(rates))


if __name__ == "__main__":
    t0 = datetime(2026, 10, 25, 9, 0)
    lot = build_lot(2, {SpotSize.SMALL: 2, SpotSize.MEDIUM: 2, SpotSize.LARGE: 1})
    car = lot.park(Vehicle("KA01AB1234", VehicleType.CAR), t0)
    bike = lot.park(Vehicle("KA05XY9", VehicleType.BIKE), t0)
    truck = lot.park(Vehicle("MH12TR77", VehicleType.TRUCK), t0)
    print(car.spot_id, bike.spot_id, truck.spot_id)  # F0-M0 F0-S0 F0-L0
    print(lot.availability()[0])
    print(lot.leave(car.ticket_id, t0 + timedelta(hours=2, minutes=5)))  # 3 started hours: 120

    try:
        lot.leave(car.ticket_id, t0 + timedelta(hours=3))
    except KeyError as e:
        print("rejected:", e)

    # 40 cars race through the gates for 6 car-sized spots (2 floors x 2 medium + 1 large)
    lot2 = build_lot(2, {SpotSize.SMALL: 2, SpotSize.MEDIUM: 2, SpotSize.LARGE: 1})
    won: list[Ticket] = []

    def arrive(i: int) -> None:
        try:
            won.append(lot2.park(Vehicle(f"DL{i:04d}", VehicleType.CAR), t0))
        except LotFullError:
            pass

    threads = [threading.Thread(target=arrive, args=(i,)) for i in range(40)]
    for th in threads:
        th.start()
    for th in threads:
        th.join()
    spots = [t.spot_id for t in won]
    print(len(won), "parked,", len(set(spots)), "distinct spots")  # 6 parked, 6 distinct spots

Run it with python3 parking_lot.py:

text5 lines
F0-M0 F0-S0 F0-L0
{'SMALL': 1, 'MEDIUM': 1, 'LARGE': 0}
Receipt(ticket_id='T0001', hours=3, amount=120)
rejected: 'unknown or already used ticket T0001'
6 parked, 6 distinct spots

The last line is the one that proves the lock: 40 threads, 6 car-sized spots, 6 tickets, no spot given twice. Remove the with self._lock: in park() and run it a few times; sooner or later two tickets share a spot.

Extending it: the follow-ups#

The last 15 minutes are follow-ups. Each one should land as a small, local change; if one forces you to rewrite ParkingLot, the design was wrong.

  • "Add an electric-vehicle spot that only EVs may use." Add SpotSize.EV (or a features set on the spot), add the rule to fits(), and write an EVFirstStrategy that tries charging spots for EVs. The lot is not touched.
  • "Weekend pricing, and the first 30 minutes free." A new PricingStrategy, or a small decorator that wraps another pricing (FreeFirst30(HourlyPricing(...))). Pricing classes compose.
  • "A display board at each entrance showing free spots per floor." Observer: the lot publishes spot_taken and spot_freed events, and each board subscribes. Don't make the board poll availability().
  • "Payment by UPI, card or cash." A PaymentMethod interface that leave() hands the receipt to. Payment failures must not free the spot until paid; that is a state on the ticket (ACTIVE → PAID → EXITED).
  • "Make allocation O(1) for a 10,000-spot lot." Keep a min-heap of free spots per size, keyed by (floor, spot_id). find_spot becomes a heap pop instead of a scan; push the spot back on leave().
  • "Lost ticket." Look the vehicle up by plate (keep a plate → ticket map, which you already need for the duplicate check) and charge a lost-ticket fee: another pricing rule.
✗ Pitfall

Common mistakes#

What costs candidates the round
  • Coding before agreeing the scope. Twenty minutes into a design the interviewer didn't ask for.
  • A ParkingLot god class with the pricing if/elif chain, the spot search and the printing all inside park().
  • A subclass per vehicle with no behaviour, and then a isinstance ladder to decide where each one fits.
  • No lock, or a lock around find_spot only, released before the spot is marked taken: the race is still there.
  • Floats for money and datetime.now() buried in the class, so the demo can't show a 3-hour bill.
  • No main. Code that was never run fails on a typo in front of the interviewer. Run it every 15 minutes.
Quick check4 questions · +5 XP each

Parking lot

  1. Q1/4 Two entry gates call park() at the same instant and both get spot F0-M0. What is the minimal correct fix?

    Answer & explanation

    Answer: B

    The race is between "I saw it free" and "I marked it taken". Locking only the search still lets both gates find the same spot before either writes. One lock around both closes the gap; per-gate floors only hide it.

  2. Q2/4 The mall wants the first 30 minutes free on weekends. In this design, what changes?

    Answer & explanation

    Answer: C

    Pricing is a strategy precisely so that a tariff change is a new class. ParkingLot is not edited (open/closed principle).

  3. Q3/4 Why is Vehicle a dataclass with a type enum instead of Car, Bike and Truck subclasses?

    Answer & explanation

    Answer: B

    Subclasses earn their place when they behave differently. Here only "what fits where" differs, and that is one rule on the spot. Empty subclasses add files, not design.

  4. Q4/4 Why does park() take the time as a parameter instead of calling datetime.now()?

    Answer & explanation

    Answer: D

    Hidden clocks make time-based rules untestable. Passing time in (or injecting a Clock) is how the demo shows "2 h 5 min is billed as 3 hours" without waiting three hours.

End of LLD 01 · Design a Parking Lot
Finish LLD 01Stages clear as you read them (0/1 so far). Skipping ahead doesn't count: marking complete unlocks after ~3 more min of reading.

New lessons by email

New lessons, side quests and case files as they ship, at most once a week. No spam, unsubscribe anytime.

Discussion · …

Public: everyone reading this lesson sees comments. Be kind and keep it on the material.

Sign in to ask a question or answer one. Reading stays open to everyone.
loading the discussion…