Design a parking lot
Multi-level lot. Mixed spot sizes (motorcycle, compact, large). Vehicles arrive, take a ticket, park somewhere appropriate, leave, pay. The interview tests how cleanly you can split "what's a vehicle," "what's a spot," "what's a ticket," and "who knows whether a spot is free." Almost every wrong answer is "the lot does everything."
1 · Clarifying questions
| Vehicle types? | Motorcycle, car, van/truck. Each takes a different spot size. |
| Spot sizes? | Motorcycle, compact, large. Big vehicles can fit small spots? Usually no — large needs large; motorcycle fits anything. |
| Lot shape? | Multi-level (floors). Each floor has a fixed set of spots in known positions. |
| How is a spot picked? | Nearest-to-entrance available spot of the right size. Could also be cheapest, or operator-chosen — clarify. |
| Payment? | By the hour; tariff per spot type. Pay at exit (or app-based). |
| Capacity? | Single-lot in scope; mall-of-lots is a separate problem. |
| Concurrency? | Yes — multiple entrances, multiple cars arriving simultaneously. |
2 · Entities
Vehicle(abstract) →Motorcycle,Car,Van. Each has asize: SpotSizeit requires.ParkingSpotwith asizeand anisAvailable()flag. Knows which floor it's on.Floor— holds a collection of spots, knows its level number, exposes "find available spot of size X".ParkingLot— holds floors, holds the entrance/exit list. The top-level coordinator.Ticket— issued on entry. Carries vehicle ref, spot ref, entered_at timestamp.PaymentProcessor— separate. Takes a ticket, computes fee, accepts payment.Display(optional) — shows "X spots free on floor Y" at the entrance.
3 · Class sketch
enum SpotSize: MOTORCYCLE, COMPACT, LARGE
abstract class Vehicle:
license: str
required_size: SpotSize # set by subclass
class Motorcycle(Vehicle): required_size = MOTORCYCLE
class Car(Vehicle): required_size = COMPACT
class Van(Vehicle): required_size = LARGE
class ParkingSpot:
spot_id: str
size: SpotSize
floor: Floor
vehicle: Vehicle | None # None means free
def assign(v): self.vehicle = v
def vacate(): self.vehicle = None
def is_free(): return self.vehicle is None
class Floor:
level: int
spots: list[ParkingSpot]
# For O(1) lookup of next free spot by size, keep three queues:
free_by_size: dict[SpotSize, Deque[ParkingSpot]]
def find_spot(size):
# Motorcycles can use any size; cars need COMPACT or LARGE; vans need LARGE.
for allowed in upgrade_path(size):
if free_by_size[allowed]:
return free_by_size[allowed].popleft()
return None
class ParkingLot:
floors: list[Floor]
active_tickets: dict[str, Ticket] # by license or by ticket_id
lock: Lock # see §6
def park(vehicle):
with lock:
for floor in self.floors:
spot = floor.find_spot(vehicle.required_size)
if spot:
spot.assign(vehicle)
ticket = Ticket(vehicle, spot, now())
active_tickets[ticket.id] = ticket
return ticket
raise LotFull()
def leave(ticket_id):
with lock:
t = active_tickets.pop(ticket_id)
t.spot.vacate()
t.spot.floor.free_by_size[t.spot.size].append(t.spot)
fee = pricing.compute(t.spot.size, now() - t.entered_at)
return fee
class Ticket:
id: str
vehicle: Vehicle
spot: ParkingSpot
entered_at: datetimeNote the small but useful trick: each floor pre-bins its free spots by size in a deque. find_spot is then O(1), and you don't iterate the whole floor on every arrival.
4 · The spot state machine
assign(v)
FREE ──────────► OCCUPIED
▲ │
│ vacate() │
└──────────────────┘
A spot also has an OUT_OF_SERVICE state for maintenance:
take_offline()
FREE ────────────► OUT_OF_SERVICE
│
│ bring_online()
▼
FREEOUT_OF_SERVICE matters more than candidates think — a real lot has spots broken by a leaky ceiling, by EV-charger installation, by reserved-for-staff signage. The state needs to exist; the search routine needs to skip those spots.
5 · Edge cases
- Van on a sea of compacts. If no large spot is available, a van waits or is turned away. Don't try to "fit" a van across two compact spots unless the requirements explicitly allow it.
- Motorcycle taking a large spot. Allowed but wasteful. The
upgrade_pathtries motorcycle spots first, falling back through sizes — so a motorcycle only takes a large spot if everything else is full. - Lost ticket. Customer arrives at exit with no ticket. Lookup by vehicle license; charge a "lost ticket" penalty rate. Don't crash.
- Spot freed mid-search. Concurrent
park()calls might both grab the same spot. The lock aroundfind_spot+assignhandles it; without the lock, you have a race. - Lot full but someone leaves while a new arrival is waiting. Either retry once or fall through to "full" — a busy lot benefits from a small wait queue.
6 · Concurrency
The single coarse lock above is correct but contended. A more realistic design:
- Per-floor lock. Each floor owns its own lock. Different floors can be parked into concurrently.
- Per-spot CAS. Each spot's
vehiclefield is atomic.find_spotpicks a candidate;assigndoes compare-and-swap (from None to vehicle). On failure, retry with the next candidate. Lock-free; works well when contention is high. - Sharded queues. Free-spot queues stay per-floor, never global. Avoids the "every car contends on one queue" pathology.
7 · Extensions
- EV chargers. Spot type carries a "has charger" flag. Pricing model treats charge-time and park-time separately.
- Reservations. Spot has a "reserved until" timestamp. Park-by-license check before assigning to a different vehicle.
- Dynamic pricing. Per-hour rate depends on occupancy. The pricing module reads current free count.
- Multi-tenant operator. One operator runs many lots.
ParkingLotbecomes one of N; the operator-level service tracks them all and supports "find any lot within 1 km with a free large spot."
8 · What gets graded
- Did you separate
Vehicle,Spot,Floor,Lot,Ticketcleanly? - Did you avoid putting "find a spot" logic inside
ParkingLotdirectly (vs. delegating toFloor)? - Did you handle the size-upgrade path (motorcycle in a large spot) deliberately?
- Did you talk about the concurrency model unprompted?
- Did you mention OUT_OF_SERVICE without being asked?