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.
| Ask | Assume (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
| Class | Its one job |
|---|---|
| ParkingLot | The entry and exit operations, under one lock |
| Spot | Floor, size, and which vehicle (if any) is in it |
| Vehicle | Plate and type; the type says which spot size it needs |
| Ticket | Issued on entry: spot and entry time |
| SpotFinder | The rule for choosing a spot (strategy) |
| FeeCalculator | The pricing rule (strategy) |
4. How it works
5. Patterns and why
| Pattern | Where | Why it's worth it here |
|---|---|---|
| Strategy | SpotFinder and FeeCalculator | New pricing or a nearest-to-the-exit rule is a new class, not an edit to ParkingLot |
| Enums with data | VehicleType knows the spot size it needs | No if/else chains on vehicle type |
| Single lock around check-and-assign | park() 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
- The race: two entrances look for a spot at the same moment, both see 1-M1 free, both assign it. Finding and assigning must happen under one lock (or as one atomic database update).
- Scaling the lock: one lock is fine for a few thousand spots. For a huge lot, keep a free-list per size with its own lock, or one lock per floor.
- Lost tickets, double exits: an unknown ticket id must be rejected, and a ticket can be used only once.
- Finding faster: scanning every spot is O(spots). A sorted set of free spots per size makes park O(log n).
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
- Add a NearestToEntranceFinder (10 min). A second SpotFinder; switch it in without editing ParkingLot.
- Add a monthly pass (10 min). A FeeCalculator decorator.