Design a Parking Lot
A city garage has several floors. Each floor mixes small bays for motorbikes, compact bays for cars and large bays for vans and trucks. A driver pulls up to an entry barrier, gets a ticket and is pointed at a bay; on the way out the ticket is scanned, the stay is priced and the barrier opens once payment succeeds.
The owners change prices often: hourly rates by vehicle type on weekdays, one flat fee on match nights, maybe a monthly pass later. They also want a live count of free bays per type for the sign at the entrance.
Design the classes for this system. In 45 minutes, agree on the scope, name the core objects and their responsibilities, walk through the entry and exit flows, and write the code for allocating a bay and pricing a stay. Be ready to explain how a new vehicle type or pricing rule would slot in.
Use the LLD board for your class diagram and the LLD code tab to write and run your classes.
Design Path · Low-Level Design · problem 17 of 18
Ask before you design. Try answering each yourself, then compare.
- Which vehicle and bay types exist, and what may park where?
Typical answer
Motorcycle, car, truck; small, compact, large bays. A vehicle may use its own size or anything larger.
- Does one vehicle ever take more than one bay?
Typical answer
No. Keep one vehicle per bay; multi-bay buses are a follow-up.
- How is a stay priced?
Typical answer
Pluggable. Default: a short free grace period, then each started hour at a rate per vehicle type. A flat event fee must be possible.
- How many entry and exit gates are there?
Typical answer
Several. Two gates must never hand out the same bay.
- Do we handle payment processing ourselves?
Typical answer
No. Treat the card terminal as a dependency that says yes or no for an amount.
- What happens when the lot is full for a vehicle type?
Typical answer
The barrier stays closed and the entrance sign shows full for that type.
- Do we need reservations or monthly passes?
Typical answer
Not now, but the design should not make them hard to add.
Requirements
Functional
- Issue a ticket on entry and assign the best fitting free bay across all levels.
- On exit, price the stay, take payment, free the bay and close the ticket.
- Support swappable pricing rules (hourly by vehicle type, flat event fee).
- Report free bays per type for the entrance display.
- Reject a vehicle when no fitting bay is free, and reject reused tickets.
Non-functional
- Allocation is correct under concurrent gates: no bay is ever given out twice.
- Entry and exit decisions in under 100 ms for a lot of up to 5,000 bays.
- Adding a vehicle type or pricing rule touches one or two classes, not the core flow.
Out of scope
- Payment gateway internals and receipts.
- Licence plate recognition hardware.
- Online pre-booking (discuss as a follow-up).
Class design
Core objects. ParkingLot is the facade the gates talk to. It owns Levels, which own ParkingSpots. A spot knows its SpotType and whether it is free; the enum itself answers "does this vehicle type fit here?", so the size rules live in one place. A Ticket records the vehicle, the spot, entry and exit times and the fee.
Entry flow. The lot asks every level for its best fitting spot, chooses the smallest bay type overall (lower level on ties), marks it taken and returns a ticket. Picking the smallest fitting bay keeps large bays free for trucks.
Exit flow. The gate scans the ticket; the lot looks it up among active tickets, asks the current PricingStrategy for the fee, calls the payment callback, then releases the spot and retires the ticket. If payment fails, nothing changes.
Extensibility. New pricing is a new strategy class set at runtime. A new vehicle size is one enum value. Allocation policy (nearest the lift, EV bays) can be pulled into its own strategy once it grows. With several gates, wrap enter and exit in a lock, or keep per-type free lists so the critical section stays tiny.
Open the LLD board and press Reference to see the class diagram.
Patterns used
- Strategy: pricing rules (hourly, flat event fee, passes) change independently of the lot, so they sit behind PricingStrategy.
- Facade: ParkingLot gives gates two calls, enter and exit, and hides levels, spots and tickets.
- Composition: Lot owns Levels, Levels own Spots, so free counts and lookups roll up naturally.
Reference implementation
from abc import ABC, abstractmethod
from datetime import datetime, timedelta
from enum import Enum
import itertools
import math
class VehicleType(Enum):
MOTORCYCLE = 1
CAR = 2
TRUCK = 3
class SpotType(Enum):
SMALL = 1 # motorcycles only
COMPACT = 2 # motorcycles and cars
LARGE = 3 # anything
def fits(self, vehicle_type: VehicleType) -> bool:
return vehicle_type.value <= self.value
class Vehicle:
def __init__(self, plate: str, vtype: VehicleType):
self.plate, self.type = plate, vtype
class ParkingSpot:
def __init__(self, spot_id: str, spot_type: SpotType):
self.id, self.type = spot_id, spot_type
self.vehicle = None
def is_free(self) -> bool:
return self.vehicle is None
def can_fit(self, v: Vehicle) -> bool:
return self.is_free() and self.type.fits(v.type)
def assign(self, v: Vehicle):
self.vehicle = v
def release(self):
self.vehicle = None
class Level:
def __init__(self, number: int, spots: list):
self.number, self.spots = number, spots
def find_spot(self, v: Vehicle):
"""Smallest free spot that fits, so trucks are not crowded out by cars."""
fitting = [s for s in self.spots if s.can_fit(v)]
return min(fitting, key=lambda s: s.type.value, default=None)
def free_count(self, spot_type: SpotType) -> int:
return sum(1 for s in self.spots if s.type == spot_type and s.is_free())
class Ticket:
_ids = itertools.count(1001)
def __init__(self, vehicle: Vehicle, spot: ParkingSpot, level: int, entry: datetime):
self.id = next(Ticket._ids)
self.vehicle, self.spot, self.level = vehicle, spot, level
self.entry, self.exit, self.fee = entry, None, None
class PricingStrategy(ABC):
@abstractmethod
def price(self, ticket: Ticket, exit_time: datetime) -> float:
...
class HourlyPricing(PricingStrategy):
"""First 15 minutes free, then every started hour is charged at a per-type rate."""
def __init__(self, rates: dict, grace=timedelta(minutes=15)):
self.rates, self.grace = rates, grace
def price(self, ticket, exit_time):
stay = exit_time - ticket.entry
if stay <= self.grace:
return 0.0
hours = math.ceil(stay.total_seconds() / 3600)
return hours * self.rates[ticket.vehicle.type]
class FlatEventPricing(PricingStrategy):
"""One fixed price per visit, e.g. on a stadium event night."""
def __init__(self, amount: float):
self.amount = amount
def price(self, ticket, exit_time):
return self.amount
class ParkingLot:
def __init__(self, levels: list, pricing: PricingStrategy):
self.levels, self.pricing = levels, pricing
self.active = {} # ticket id -> Ticket
def set_pricing(self, pricing: PricingStrategy):
self.pricing = pricing
def enter(self, v: Vehicle, now: datetime):
if any(t.vehicle.plate == v.plate for t in self.active.values()):
raise ValueError(f"{v.plate} is already inside")
# Smallest fitting spot anywhere in the lot, lowest level on ties.
options = [(spot, level) for level in self.levels if (spot := level.find_spot(v))]
if not options:
return None # full for this vehicle type: barrier stays closed
spot, level = min(options, key=lambda o: (o[0].type.value, o[1].number))
spot.assign(v)
ticket = Ticket(v, spot, level.number, now)
self.active[ticket.id] = ticket
return ticket
def exit(self, ticket_id: int, now: datetime, pay) -> float:
ticket = self.active.get(ticket_id)
if ticket is None:
raise KeyError(f"unknown or already used ticket {ticket_id}")
fee = self.pricing.price(ticket, now)
if fee and not pay(fee):
raise RuntimeError("payment declined; barrier stays closed")
ticket.exit, ticket.fee = now, fee
ticket.spot.release()
del self.active[ticket_id]
return fee
def availability(self) -> dict:
return {t.name: sum(l.free_count(t) for l in self.levels) for t in SpotType}
if __name__ == "__main__":
def make_level(n):
spots = [ParkingSpot(f"L{n}-S{i}", SpotType.SMALL) for i in range(1)]
spots += [ParkingSpot(f"L{n}-C{i}", SpotType.COMPACT) for i in range(2)]
spots += [ParkingSpot(f"L{n}-L{i}", SpotType.LARGE) for i in range(1)]
return Level(n, spots)
rates = {VehicleType.MOTORCYCLE: 1.0, VehicleType.CAR: 2.5, VehicleType.TRUCK: 5.0}
lot = ParkingLot([make_level(0), make_level(1)], HourlyPricing(rates))
card = lambda amount: print(f" card charged ${amount:.2f}") or True
t0 = datetime(2026, 3, 14, 8, 0)
arrivals = [Vehicle("MOTO-7", VehicleType.MOTORCYCLE), Vehicle("CAR-1", VehicleType.CAR),
Vehicle("CAR-2", VehicleType.CAR), Vehicle("CAR-3", VehicleType.CAR),
Vehicle("TRUCK-9", VehicleType.TRUCK), Vehicle("TRUCK-10", VehicleType.TRUCK),
Vehicle("TRUCK-11", VehicleType.TRUCK)]
tickets = {}
for i, v in enumerate(arrivals):
t = lot.enter(v, t0 + timedelta(minutes=5 * i))
if t:
tickets[v.plate] = t
print(f"{v.plate:9} -> ticket {t.id}, level {t.level}, spot {t.spot.id}")
else:
print(f"{v.plate:9} -> no fitting spot, turned away")
print("free spots:", lot.availability())
print("CAR-1 leaves after 2h10m (3 started hours):")
print(" fee", lot.exit(tickets["CAR-1"].id, t0 + timedelta(hours=2, minutes=15), card))
print("MOTO-7 leaves after 10 minutes:")
print(" fee", lot.exit(tickets["MOTO-7"].id, t0 + timedelta(minutes=10), card))
lot.set_pricing(FlatEventPricing(20.0))
print("Event pricing switched on; TRUCK-9 leaves:")
print(" fee", lot.exit(tickets["TRUCK-9"].id, t0 + timedelta(hours=5), card))
try:
lot.exit(tickets["TRUCK-9"].id, t0 + timedelta(hours=6), card)
except KeyError as e:
print("reusing a ticket is rejected:", e)
print("free spots:", lot.availability())
Trade-offs
- Scanning levels for a free bay is simple and fine for thousands of bays, but per-type free lists give O(1) allocation at the cost of keeping two structures in sync.
- One global lock is easy to reason about but serialises all gates; per-level locks allow parallel entries at the cost of more careful ordering.
- Letting smaller vehicles use bigger bays raises utilisation but can starve trucks; always taking the smallest fit reduces that risk.
- An enum for spot size keeps rules in one place, while class-per-size gives polymorphism you rarely need here.
What a strong answer shows
- Puts each rule with the object that owns the data (fit check on the spot type, fee on the pricing strategy).
- Keeps pricing pluggable without touching entry or exit code.
- Chooses the smallest fitting bay and can explain why.
- Handles the unhappy paths: full lot, declined payment, reused or unknown ticket.
- Mentions concurrency between gates and where the lock goes.
- Writes clean, runnable code for allocation and pricing within the time.
Mistakes to avoid
- A giant ParkingLot class with if/else chains on vehicle type for both fitting and pricing.
- Subclassing ParkingSpot per size when an enum attribute is enough.
- Releasing the bay before payment succeeds, which lets a car leave without paying.
- Spending most of the time on payment, display boards or UI instead of the core model.
- Ignoring that two gates can grab the same bay at once.
Follow-ups to expect
- How would you add electric bays with chargers? (A spot feature flag plus an allocation strategy that prefers them for EVs.)
- How would monthly passes work? (A pricing strategy that returns zero for valid pass holders, checked by plate.)
- How do you make allocation O(1)? (Keep a free-list or heap per spot type instead of scanning.)
- A bus needs five adjacent large bays. What changes? (Spots get positions; allocation finds a run of free neighbours.)