Design a Parking Lot
1. Problem & Why It's Asked
A classic LLD warm-up because it forces the fundamentals without much domain complexity: multiple entity types (vehicles, spots, tickets), a matching/allocation rule, and an obvious extensibility follow-up (add floors, add EV charging, add pricing tiers). It's a filter for whether you reach for polymorphism or write a wall of if/elif on type strings.
2. Requirements
Functional:
- Vehicles of different types (motorcycle, car, truck/bus) park in spots sized for their type.
- Issue a ticket on entry; compute a fee on exit based on duration.
- Track available spots per type, support multiple floors.
Non-functional:
- Thread-safe — two vehicles must never be assigned the same spot concurrently.
- Extensible — new vehicle types, new pricing schemes, new spot types (EV charging) shouldn't require touching existing classes (Open/Closed — see [[SOLID Principles]]).
3. Core Entities & Class Diagram
classDiagram
class VehicleType {
<<enumeration>>
MOTORCYCLE
CAR
TRUCK
}
class Vehicle {
+str plate
+VehicleType type
}
class ParkingSpot {
+str id
+VehicleType type
+int floor
+Vehicle vehicle
+is_free() bool
+park(vehicle)
+vacate()
}
class Ticket {
+str id
+str spot_id
+str plate
+datetime entry_time
+datetime exit_time
}
class PricingStrategy {
<<interface>>
+calculate_fee(ticket) float
}
class ParkingLot {
-dict~str,ParkingSpot~ spots
-dict~str,Ticket~ active_tickets
-PricingStrategy pricing
-Lock lock
+park(vehicle) Ticket
+unpark(ticket_id) float
}
Vehicle "1" --> "1" VehicleType
ParkingSpot "1" --> "0..1" Vehicle
ParkingLot "1" --> "*" ParkingSpot
ParkingLot "1" --> "*" Ticket
ParkingLot --> PricingStrategy
4. Design Patterns Used
- Strategy for pricing — different fee schemes (flat rate, per-hour, per-vehicle-type) plug in without changing
ParkingLot(see [[OOD Design Patterns]]). - Factory (optional, for larger versions) — constructing the right
ParkingSpotsubtype per floor-plan config. - Polymorphic
VehicleTypematching over spots, rather than branchingif vehicle.type == "car": ...scattered through the allocation logic — this is the Open/Closed signal interviewers look for.
5. Implementation
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from threading import Lock
from uuid import uuid4
class VehicleType(Enum):
MOTORCYCLE = "motorcycle"
CAR = "car"
TRUCK = "truck"
@dataclass
class Vehicle:
plate: str
type: VehicleType
class ParkingSpot:
def __init__(self, spot_id: str, spot_type: VehicleType, floor: int):
self.id = spot_id
self.type = spot_type
self.floor = floor
self.vehicle: Vehicle | None = None
@property
def is_free(self) -> bool:
return self.vehicle is None
def park(self, vehicle: Vehicle) -> None:
self.vehicle = vehicle
def vacate(self) -> None:
self.vehicle = None
@dataclass
class Ticket:
id: str
spot_id: str
plate: str
entry_time: datetime
exit_time: datetime | None = None
class PricingStrategy(ABC):
@abstractmethod
def calculate_fee(self, ticket: Ticket) -> float: ...
class HourlyPricing(PricingStrategy):
def __init__(self, rate_per_hour: float = 2.0):
self.rate = rate_per_hour
def calculate_fee(self, ticket: Ticket) -> float:
duration = (ticket.exit_time - ticket.entry_time).total_seconds() / 3600
return round(max(duration, 0.25) * self.rate, 2) # 15-min minimum charge
class ParkingLot:
def __init__(self, pricing: PricingStrategy):
self.spots: dict[str, ParkingSpot] = {}
self.active_tickets: dict[str, Ticket] = {}
self.pricing = pricing
self._lock = Lock()
def add_spot(self, spot: ParkingSpot) -> None:
self.spots[spot.id] = spot
def park(self, vehicle: Vehicle) -> Ticket | None:
with self._lock: # atomic find-and-claim
for spot in self.spots.values():
if spot.is_free and spot.type == vehicle.type:
spot.park(vehicle)
ticket = Ticket(
id=str(uuid4()), spot_id=spot.id,
plate=vehicle.plate, entry_time=datetime.now(),
)
self.active_tickets[ticket.id] = ticket
return ticket
return None # lot full for this vehicle type
def unpark(self, ticket_id: str) -> float:
with self._lock:
ticket = self.active_tickets.pop(ticket_id)
ticket.exit_time = datetime.now()
self.spots[ticket.spot_id].vacate()
return self.pricing.calculate_fee(ticket)
6. Concurrency
- The naive version (existing vault note) had no locking — two threads calling
park()simultaneously could both pass theis_freecheck on the same spot before either setsspot.vehicle, double-booking it. TheLock()around the whole find-and-claim inpark()closes this window. - Granularity trade-off: one lock for the entire lot serializes all park/unpark calls — correct, but a bottleneck at high throughput (an airport lot with thousands of spots). A per-spot lock (or per-floor) would allow concurrent parks in different sections; the single-lock version is the right starting answer, with this as the scaling follow-up.
unpark()uses.pop()on the ticket dict — calling it twice on the sameticket_idraisesKeyErrorrather than silently double-charging or double-vacating; worth naming this as deliberate (fail loud on a re-used ticket) rather than an oversight.
7. Edge Cases
- Lot full for a vehicle's type —
park()returnsNone; caller must handle (reject entry, or queue). - Lost/duplicate ticket —
unpark()on an unknownticket_idshould raise a clear error, not silently no-op. - Vehicle larger than any available spot type — requirements should clarify whether a truck can use a car spot (usually no) or whether spot-type matching is strict; the implementation above assumes strict matching.
8. Extensibility
- New vehicle type (e.g., bicycle): add an enum value + spots of that type — zero changes to
ParkingLot.park()logic, since matching is by enum equality, not a type-specific branch. - New pricing scheme (e.g., flat daily rate, first-hour-free): implement a new
PricingStrategysubclass, pass it intoParkingLot.__init__— zero changes toParkingLotitself. This is the concrete payoff of using Strategy from the start. - EV charging spots: add a
requires_charging: boolflag toParkingSpotand extend matching logic — or, for a cleaner OCP-compliant version, introduce aSpotFeatureset on each spot and match on required features rather than a singleVehicleTypeenum, avoiding a combinatorial explosion of enum values (CAR,CAR_EV,CAR_HANDICAP,CAR_EV_HANDICAP, ...). - Multi-floor display board ("2 spots free on floor 3"): an
Observer(see [[OOD Design Patterns]]) subscribed topark()/unpark()events, decoupled from the core allocation logic.
9. Interview Drills
- Two cars arrive at the exact same moment for the last free spot. Walk through what happens with your code. — Both threads call
park(); theLockserializes them, so the first to acquire it claims the spot and returns aTicket, the second finds no free spot of that type after acquiring the lock next and returnsNone. Without the lock, both could readis_free=Truebefore either writes, double-booking. - How would you support a spot that fits multiple vehicle types (a large spot that also fits motorcycles)? — Change
ParkingSpot.typefrom a singleVehicleTypeto a set of accepted types, and matchvehicle.type in spot.accepted_types. - Add support for reserved/subscription spots that regular vehicles can't use. — Add a
reserved_for: str | Nonefield onParkingSpot; matching logic checksreserved_for is None or reserved_for == vehicle.owner_idalongside the type check — no change to the surrounding allocation flow.
Cross-links
[[SOLID Principles]] · [[OOD Design Patterns]] · [[LLD Interview Approach]] · [[Rate Limiter (OOD)]]