Design a parking lot
Requirements
Section titled “Requirements”Ask before you draw. For this version we agree on:
- Three spot sizes (small, medium, large) and three vehicle types (motorcycle, car, truck).
- A vehicle can park in any spot at least as big as it needs.
- Parking returns a ticket; leaving with that ticket frees the spot.
- Two vehicles arriving at the same moment must never get the same spot.
- Out of scope for now: payments, multiple floors, reservations.
Entities
Section titled “Entities”Vehicle, ParkingSpot, Ticket and ParkingLot fall straight out of the requirements. The
one that doesn’t is SpotAssignmentStrategy. Interviewers almost always follow up with “what if
we want to fill the nearest spot first instead?”, and pulling that decision into its own type
means the answer is a new class, not an edit to ParkingLot.
Class diagram
Section titled “Class diagram”classDiagram
class ParkingLot {
-List~ParkingSpot~ spots
-Map~String,Ticket~ active
+park(Vehicle) Optional~Ticket~
+unpark(ticketId)
}
class SpotAssignmentStrategy {
<<interface>>
+pick(spots, vehicle) Optional~ParkingSpot~
}
class BestFitStrategy
class ParkingSpot {
+id
+size: SpotSize
+fits(Vehicle) bool
}
class Vehicle {
+plate
+type: VehicleType
}
class Ticket {
+id
+entry: Instant
}
ParkingLot o-- ParkingSpot
ParkingLot --> SpotAssignmentStrategy
SpotAssignmentStrategy <|.. BestFitStrategy
ParkingLot --> Ticket : issues
Ticket --> Vehicle
Ticket --> ParkingSpotimport java.time.Instant;import java.util.*;
enum SpotSize { SMALL, MEDIUM, LARGE }
enum VehicleType { MOTORCYCLE(SpotSize.SMALL), CAR(SpotSize.MEDIUM), TRUCK(SpotSize.LARGE);
final SpotSize minSpot; VehicleType(SpotSize minSpot) { this.minSpot = minSpot; }}
record Vehicle(String plate, VehicleType type) {}
final class ParkingSpot { final String id; final SpotSize size; private Vehicle parked;
ParkingSpot(String id, SpotSize size) { this.id = id; this.size = size; }
boolean fits(Vehicle v) { return parked == null && size.compareTo(v.type().minSpot) >= 0; } void park(Vehicle v) { parked = v; } void free() { parked = null; }}
record Ticket(String id, Vehicle vehicle, ParkingSpot spot, Instant entry) {}
/** Decides which free spot a vehicle gets. Swappable without touching ParkingLot. */interface SpotAssignmentStrategy { Optional<ParkingSpot> pick(List<ParkingSpot> spots, Vehicle v);}
/** Smallest spot that fits, so large spots stay free for trucks. */final class BestFitStrategy implements SpotAssignmentStrategy { public Optional<ParkingSpot> pick(List<ParkingSpot> spots, Vehicle v) { return spots.stream() .filter(s -> s.fits(v)) .min(Comparator.comparing(s -> s.size)); }}
public final class ParkingLot { private final List<ParkingSpot> spots; private final SpotAssignmentStrategy strategy; private final Map<String, Ticket> active = new HashMap<>();
public ParkingLot(List<ParkingSpot> spots, SpotAssignmentStrategy strategy) { this.spots = List.copyOf(spots); this.strategy = strategy; }
// synchronized: two cars must never get the same spot. See "Alternatives" for finer locking. public synchronized Optional<Ticket> park(Vehicle v) { return strategy.pick(spots, v).map(spot -> { spot.park(v); Ticket t = new Ticket(UUID.randomUUID().toString(), v, spot, Instant.now()); active.put(t.id(), t); return t; }); }
public synchronized void unpark(String ticketId) { Ticket t = active.remove(ticketId); if (t == null) throw new IllegalArgumentException("Unknown ticket " + ticketId); t.spot().free(); }
public static void main(String[] args) { ParkingLot lot = new ParkingLot( List.of(new ParkingSpot("S1", SpotSize.SMALL), new ParkingSpot("M1", SpotSize.MEDIUM), new ParkingSpot("L1", SpotSize.LARGE)), new BestFitStrategy()); Ticket bike = lot.park(new Vehicle("B-1", VehicleType.MOTORCYCLE)).orElseThrow(); Ticket car = lot.park(new Vehicle("C-1", VehicleType.CAR)).orElseThrow(); System.out.println(bike.spot().id + " " + car.spot().id); // S1 M1 lot.unpark(car.id()); }}from __future__ import annotations
import threadingimport uuidfrom dataclasses import dataclass, fieldfrom datetime import datetime, timezonefrom enum import Enumfrom typing import Protocol
class SpotSize(Enum): SMALL = 1 MEDIUM = 2 LARGE = 3
class VehicleType(Enum): MOTORCYCLE = SpotSize.SMALL CAR = SpotSize.MEDIUM TRUCK = SpotSize.LARGE
@property def min_spot(self) -> SpotSize: return self.value
@dataclass(frozen=True)class Vehicle: plate: str type: VehicleType
@dataclassclass ParkingSpot: id: str size: SpotSize parked: Vehicle | None = None
def fits(self, v: Vehicle) -> bool: return self.parked is None and self.size.value >= v.type.min_spot.value
@dataclass(frozen=True)class Ticket: id: str vehicle: Vehicle spot: ParkingSpot entry: datetime
class SpotAssignmentStrategy(Protocol): """Decides which free spot a vehicle gets. Swappable without touching ParkingLot."""
def pick(self, spots: list[ParkingSpot], v: Vehicle) -> ParkingSpot | None: ...
class BestFitStrategy: """Smallest spot that fits, so large spots stay free for trucks."""
def pick(self, spots: list[ParkingSpot], v: Vehicle) -> ParkingSpot | None: free = [s for s in spots if s.fits(v)] return min(free, key=lambda s: s.size.value, default=None)
@dataclassclass ParkingLot: spots: list[ParkingSpot] strategy: SpotAssignmentStrategy _active: dict[str, Ticket] = field(default_factory=dict) # One lock: two cars must never get the same spot. See "Alternatives" for finer locking. _lock: threading.Lock = field(default_factory=threading.Lock)
def park(self, v: Vehicle) -> Ticket | None: with self._lock: spot = self.strategy.pick(self.spots, v) if spot is None: return None spot.parked = v ticket = Ticket(str(uuid.uuid4()), v, spot, datetime.now(timezone.utc)) self._active[ticket.id] = ticket return ticket
def unpark(self, ticket_id: str) -> None: with self._lock: ticket = self._active.pop(ticket_id, None) if ticket is None: raise KeyError(f"Unknown ticket {ticket_id}") ticket.spot.parked = None
if __name__ == "__main__": lot = ParkingLot( [ParkingSpot("S1", SpotSize.SMALL), ParkingSpot("M1", SpotSize.MEDIUM), ParkingSpot("L1", SpotSize.LARGE)], BestFitStrategy(), ) bike = lot.park(Vehicle("B-1", VehicleType.MOTORCYCLE)) car = lot.park(Vehicle("C-1", VehicleType.CAR)) print(bike.spot.id, car.spot.id) # S1 M1 lot.unpark(car.id)Alternatives
Section titled “Alternatives”The common upgrade is one free-spot queue per size, each with its own lock, so a motorcycle and a truck never contend. That trades simplicity for throughput, and it makes “best fit” harder because you now check several queues in order.
Extensions
Section titled “Extensions”- Multiple floors: a
Levelthat owns spots, with the strategy choosing a level first. - Pricing: a
PricingPolicycomputed from the ticket’s entry time when the vehicle leaves. - Display boards showing free spots per size, updated by events instead of polling.