InterviewKata
Low-level design · Problem P06 of 15 · Classic systems

Parking lot

The best-known low-level design question. The classes are easy to list; what separates answers is where the rules live (which spot? how much?) and whether two cars can ever get the same spot.

1. The prompt

The prompt"Design a parking lot with several floors and spots of different sizes. Vehicles of different types arrive and leave; issue a ticket on entry and charge on exit."

2. Clarify the requirements

Spend the first five minutes here. Ask, then write the answers down where the interviewer can see them.

AskAssume (unless told otherwise)
Vehicle types and sizes?Motorcycle → small, car → medium, truck → large; a vehicle can use a bigger spot.
Which spot is chosen?Smallest that fits, then lowest floor; make it swappable.
How is the fee computed?Per started hour, rate by vehicle type; make it swappable.
Several entrances?Yes, so parking must be thread-safe.

Must do

  • Park a vehicle in a spot that fits; issue a ticket
  • On exit, free the spot and compute the fee
  • Report when nothing fits

Must be

  • Never give one spot to two vehicles
  • Change the spot rule or pricing without editing the lot
  • Fast enough for a few thousand spots

Out of scope

  • Payment processing
  • Reservations
  • Display boards and sensors (follow-ups)

3. The classes

The class diagram: arrows show who uses or holds whom.
ClassIts one job
ParkingLotThe entry and exit operations, under one lock
SpotFloor, size, and which vehicle (if any) is in it
VehiclePlate and type; the type says which spot size it needs
TicketIssued on entry: spot and entry time
SpotFinderThe rule for choosing a spot (strategy)
FeeCalculatorThe pricing rule (strategy)

4. How it works

5. Patterns and why

PatternWhereWhy it's worth it here
StrategySpotFinder and FeeCalculatorNew pricing or a nearest-to-the-exit rule is a new class, not an edit to ParkingLot
Enums with dataVehicleType knows the spot size it needsNo if/else chains on vehicle type
Single lock around check-and-assignpark() and leave()Two cars can never be given the same spot
Memory hook: "Find, hold, ticket; then free and price"Entry finds the smallest spot that fits and records it on a ticket; exit frees the spot and asks the fee strategy what to charge.

6. The code

Money is in integer cents. The finder and fee rules are passed in, so the demo could swap either without touching ParkingLot.

from __future__ import annotations
import itertools
import threading
from abc import ABC, abstractmethod
from dataclasses import dataclass
from enum import Enum
from typing import Optional


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


class VehicleType(Enum):
    MOTORCYCLE = SpotSize.SMALL
    CAR = SpotSize.MEDIUM
    TRUCK = SpotSize.LARGE


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


@dataclass
class Spot:
    id: str
    floor: int
    size: SpotSize
    vehicle: Optional[Vehicle] = None

    def fits(self, v: Vehicle) -> bool:
        return self.vehicle is None and self.size.value >= v.type.value.value


@dataclass(frozen=True)
class Ticket:
    id: int
    plate: str
    spot_id: str
    entry_min: int


class SpotFinder(ABC):
    @abstractmethod
    def find(self, spots: list[Spot], v: Vehicle) -> Optional[Spot]: ...


class SmallestFitFinder(SpotFinder):
    """Smallest spot that fits, then the lowest floor: keeps big spots free for big vehicles."""

    def find(self, spots: list[Spot], v: Vehicle) -> Optional[Spot]:
        free = [s for s in spots if s.fits(v)]
        return min(free, key=lambda s: (s.size.value, s.floor, s.id), default=None)


class FeeCalculator(ABC):
    @abstractmethod
    def fee_cents(self, v: Vehicle, minutes: int) -> int: ...


class HourlyFee(FeeCalculator):
    """Every started hour is charged; the rate depends on the vehicle."""

    RATES = {VehicleType.MOTORCYCLE: 100, VehicleType.CAR: 200, VehicleType.TRUCK: 500}

    def fee_cents(self, v: Vehicle, minutes: int) -> int:
        hours = max(1, -(-minutes // 60))
        return hours * self.RATES[v.type]


class ParkingLot:
    def __init__(self, spots: list[Spot], finder: SpotFinder, fees: FeeCalculator) -> None:
        self.spots = {s.id: s for s in spots}
        self.finder, self.fees = finder, fees
        self.tickets: dict[int, tuple[Ticket, Vehicle]] = {}
        self.ids = itertools.count(1)
        self.lock = threading.Lock()   # two cars must never get the same spot

    def park(self, v: Vehicle, now_min: int) -> Optional[Ticket]:
        with self.lock:
            spot = self.finder.find(list(self.spots.values()), v)
            if spot is None:
                return None
            spot.vehicle = v
            t = Ticket(next(self.ids), v.plate, spot.id, now_min)
            self.tickets[t.id] = (t, v)
            return t

    def leave(self, ticket_id: int, now_min: int) -> int:
        with self.lock:
            t, v = self.tickets.pop(ticket_id)
            self.spots[t.spot_id].vehicle = None
            return self.fees.fee_cents(v, now_min - t.entry_min)


def money(cents: int) -> str:
    return f"${cents // 100}.{cents % 100:02d}"


if __name__ == "__main__":
    S, M, L = SpotSize.SMALL, SpotSize.MEDIUM, SpotSize.LARGE
    lot = ParkingLot([Spot("1-S1", 1, S), Spot("1-M1", 1, M), Spot("1-M2", 1, M), Spot("1-L1", 1, L), Spot("2-M1", 2, M)],
                     SmallestFitFinder(), HourlyFee())
    arrivals = [Vehicle("MOTO-1", VehicleType.MOTORCYCLE), Vehicle("CAR-A", VehicleType.CAR), Vehicle("CAR-B", VehicleType.CAR),
                Vehicle("TRUCK-1", VehicleType.TRUCK), Vehicle("CAR-C", VehicleType.CAR), Vehicle("CAR-D", VehicleType.CAR)]
    tickets = {}
    for v in arrivals:
        t = lot.park(v, 0)
        tickets[v.plate] = t
        print(f"{v.plate} -> {t.spot_id if t else 'lot full for this size'}")
    print("CAR-A leaves after 130 min, pays", money(lot.leave(tickets["CAR-A"].id, 130)))
    t = lot.park(Vehicle("CAR-D", VehicleType.CAR), 131)
    print("CAR-D ->", t.spot_id if t else "lot full")
enum SpotSize { SMALL = 1, MEDIUM = 2, LARGE = 3 }
enum VehicleType { MOTORCYCLE = "MOTORCYCLE", CAR = "CAR", TRUCK = "TRUCK" }
const NEEDS: Record<VehicleType, SpotSize> = { MOTORCYCLE: SpotSize.SMALL, CAR: SpotSize.MEDIUM, TRUCK: SpotSize.LARGE };

interface Vehicle { plate: string; type: VehicleType; }

class Spot {
  vehicle: Vehicle | null = null;
  constructor(readonly id: string, readonly floor: number, readonly size: SpotSize) {}
  fits(v: Vehicle): boolean { return this.vehicle === null && this.size >= NEEDS[v.type]; }
}

interface Ticket { id: number; plate: string; spotId: string; entryMin: number; }

interface SpotFinder { find(spots: Spot[], v: Vehicle): Spot | null; }

/** Smallest spot that fits, then the lowest floor: keeps big spots free for big vehicles. */
class SmallestFitFinder implements SpotFinder {
  find(spots: Spot[], v: Vehicle): Spot | null {
    const free = spots.filter((s) => s.fits(v));
    free.sort((a, b) => a.size - b.size || a.floor - b.floor || a.id.localeCompare(b.id));
    return free[0] ?? null;
  }
}

interface FeeCalculator { feeCents(v: Vehicle, minutes: number): number; }

/** Every started hour is charged; the rate depends on the vehicle. */
class HourlyFee implements FeeCalculator {
  private static RATES: Record<VehicleType, number> = { MOTORCYCLE: 100, CAR: 200, TRUCK: 500 };
  feeCents(v: Vehicle, minutes: number): number {
    return Math.max(1, Math.ceil(minutes / 60)) * HourlyFee.RATES[v.type];
  }
}

class ParkingLot {
  private spots: Map<string, Spot>;
  private tickets = new Map<number, { t: Ticket; v: Vehicle }>();
  private nextId = 1;

  constructor(spots: Spot[], private finder: SpotFinder, private fees: FeeCalculator) {
    this.spots = new Map(spots.map((s) => [s.id, s]));
  }

  park(v: Vehicle, nowMin: number): Ticket | null {
    const spot = this.finder.find([...this.spots.values()], v);
    if (!spot) return null;
    spot.vehicle = v;
    const t: Ticket = { id: this.nextId++, plate: v.plate, spotId: spot.id, entryMin: nowMin };
    this.tickets.set(t.id, { t, v });
    return t;
  }

  leave(ticketId: number, nowMin: number): number {
    const entry = this.tickets.get(ticketId);
    if (!entry) throw new Error("unknown ticket");
    this.tickets.delete(ticketId);
    this.spots.get(entry.t.spotId)!.vehicle = null;
    return this.fees.feeCents(entry.v, nowMin - entry.t.entryMin);
  }
}

const money = (c: number) => `$${Math.floor(c / 100)}.${String(c % 100).padStart(2, "0")}`;

const lot = new ParkingLot([new Spot("1-S1", 1, SpotSize.SMALL), new Spot("1-M1", 1, SpotSize.MEDIUM), new Spot("1-M2", 1, SpotSize.MEDIUM),
  new Spot("1-L1", 1, SpotSize.LARGE), new Spot("2-M1", 2, SpotSize.MEDIUM)], new SmallestFitFinder(), new HourlyFee());
const arrivals: Vehicle[] = [["MOTO-1", VehicleType.MOTORCYCLE], ["CAR-A", VehicleType.CAR], ["CAR-B", VehicleType.CAR],
  ["TRUCK-1", VehicleType.TRUCK], ["CAR-C", VehicleType.CAR], ["CAR-D", VehicleType.CAR]].map(([plate, type]) => ({ plate, type: type as VehicleType }));
const tickets = new Map<string, Ticket | null>();
for (const v of arrivals) {
  const t = lot.park(v, 0);
  tickets.set(v.plate, t);
  console.log(`${v.plate} -> ${t ? t.spotId : "lot full for this size"}`);
}
console.log("CAR-A leaves after 130 min, pays", money(lot.leave(tickets.get("CAR-A")!.id, 130)));
const t = lot.park({ plate: "CAR-D", type: VehicleType.CAR }, 131);
console.log("CAR-D ->", t ? t.spotId : "lot full");
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

enum SpotSize { SMALL, MEDIUM, LARGE }

enum VehicleType {
    MOTORCYCLE(SpotSize.SMALL), CAR(SpotSize.MEDIUM), TRUCK(SpotSize.LARGE);

    final SpotSize needs;

    VehicleType(SpotSize needs) {
        this.needs = needs;
    }
}

record Vehicle(String plate, VehicleType type) {}

class Spot {
    final String id;
    final int floor;
    final SpotSize size;
    Vehicle vehicle;

    Spot(String id, int floor, SpotSize size) {
        this.id = id;
        this.floor = floor;
        this.size = size;
    }

    boolean fits(Vehicle v) {
        return vehicle == null && size.compareTo(v.type().needs) >= 0;
    }
}

record Ticket(int id, String plate, String spotId, int entryMin) {}

interface SpotFinder {
    Optional<Spot> find(List<Spot> spots, Vehicle v);
}

/** Smallest spot that fits, then the lowest floor: keeps big spots free for big vehicles. */
class SmallestFitFinder implements SpotFinder {
    public Optional<Spot> find(List<Spot> spots, Vehicle v) {
        return spots.stream().filter(s -> s.fits(v))
                .min(Comparator.comparing((Spot s) -> s.size).thenComparingInt(s -> s.floor).thenComparing(s -> s.id));
    }
}

interface FeeCalculator {
    int feeCents(Vehicle v, int minutes);
}

/** Every started hour is charged; the rate depends on the vehicle. */
class HourlyFee implements FeeCalculator {
    private static final Map<VehicleType, Integer> RATES = Map.of(VehicleType.MOTORCYCLE, 100, VehicleType.CAR, 200, VehicleType.TRUCK, 500);

    public int feeCents(Vehicle v, int minutes) {
        int hours = Math.max(1, (minutes + 59) / 60);
        return hours * RATES.get(v.type());
    }
}

class ParkingLot {
    private record Entry(Ticket ticket, Vehicle vehicle) {}

    private final Map<String, Spot> spots = new LinkedHashMap<>();
    private final Map<Integer, Entry> tickets = new HashMap<>();
    private final SpotFinder finder;
    private final FeeCalculator fees;
    private int nextId = 1;

    ParkingLot(List<Spot> spots, SpotFinder finder, FeeCalculator fees) {
        spots.forEach(s -> this.spots.put(s.id, s));
        this.finder = finder;
        this.fees = fees;
    }

    /** synchronized: two cars must never get the same spot. */
    synchronized Optional<Ticket> park(Vehicle v, int nowMin) {
        Optional<Spot> spot = finder.find(new ArrayList<>(spots.values()), v);
        if (spot.isEmpty()) return Optional.empty();
        spot.get().vehicle = v;
        Ticket t = new Ticket(nextId++, v.plate(), spot.get().id, nowMin);
        tickets.put(t.id(), new Entry(t, v));
        return Optional.of(t);
    }

    synchronized int leave(int ticketId, int nowMin) {
        Entry e = tickets.remove(ticketId);
        if (e == null) throw new IllegalArgumentException("unknown ticket");
        spots.get(e.ticket().spotId()).vehicle = null;
        return fees.feeCents(e.vehicle(), nowMin - e.ticket().entryMin());
    }
}

public class Main {
    static String money(int c) {
        return "$" + (c / 100) + "." + (c % 100 < 10 ? "0" : "") + (c % 100);
    }

    public static void main(String[] args) {
        ParkingLot lot = new ParkingLot(List.of(new Spot("1-S1", 1, SpotSize.SMALL), new Spot("1-M1", 1, SpotSize.MEDIUM),
                new Spot("1-M2", 1, SpotSize.MEDIUM), new Spot("1-L1", 1, SpotSize.LARGE), new Spot("2-M1", 2, SpotSize.MEDIUM)),
                new SmallestFitFinder(), new HourlyFee());
        List<Vehicle> arrivals = List.of(new Vehicle("MOTO-1", VehicleType.MOTORCYCLE), new Vehicle("CAR-A", VehicleType.CAR),
                new Vehicle("CAR-B", VehicleType.CAR), new Vehicle("TRUCK-1", VehicleType.TRUCK),
                new Vehicle("CAR-C", VehicleType.CAR), new Vehicle("CAR-D", VehicleType.CAR));
        Map<String, Ticket> tickets = new HashMap<>();
        for (Vehicle v : arrivals) {
            Optional<Ticket> t = lot.park(v, 0);
            t.ifPresent(x -> tickets.put(v.plate(), x));
            System.out.println(v.plate() + " -> " + t.map(Ticket::spotId).orElse("lot full for this size"));
        }
        System.out.println("CAR-A leaves after 130 min, pays " + money(lot.leave(tickets.get("CAR-A").id(), 130)));
        System.out.println("CAR-D -> " + lot.park(new Vehicle("CAR-D", VehicleType.CAR), 131).map(Ticket::spotId).orElse("lot full"));
    }
}

Output (the same in all three languages)

MOTO-1 -> 1-S1
CAR-A -> 1-M1
CAR-B -> 1-M2
TRUCK-1 -> 1-L1
CAR-C -> 2-M1
CAR-D -> lot full for this size
CAR-A leaves after 130 min, pays $6.00
CAR-D -> 1-M1

7. Concurrency and edge cases

8. Follow-up questions

What interviewers ask once the core works, and a short answer for each.

How would you show free spots per floor on a display?
Observer: the lot publishes spot-taken and spot-freed events; each floor's display subscribes and updates its count.
Electric vehicle charging spots?
A spot feature (charger) and a finder rule that prefers charger spots for EVs; the fee strategy adds energy used.
Monthly pass holders?
A FeeCalculator decorator that checks for a valid pass and charges zero.
How would you persist it?
Spots and tickets as tables; parking becomes a conditional update ("set vehicle where id = ? and vehicle is null") that only one entrance can win.

9. Common mistakes

A Car class that extends Vehicle for every typeBehaviour doesn't differ by type, only the size needed. An enum with data is enough.
Pricing inside ParkingLotEvery pricing change edits the core class. Put it behind an interface.
Check-then-assign without a lockThe classic race: two cars, one spot.
Floats for moneyUse integer cents.

10. Check yourself

Two entrances assign spots at the same moment. What prevents a double assignment?

Check-then-act must be atomic.

Management wants weekend pricing. What changes?

Pricing is a strategy, so the lot doesn't change.

Why choose the smallest spot that fits?

A car in the only large spot would turn away the next truck.

Your turn

  1. Add a NearestToEntranceFinder (10 min). A second SpotFinder; switch it in without editing ParkingLot.
  2. Add a monthly pass (10 min). A FeeCalculator decorator.