02 / 07
OOD / 02

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 a size: SpotSize it requires.
  • ParkingSpot with a size and an isAvailable() 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: datetime

Note 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()
                              ▼
                            FREE

OUT_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_path tries 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 around find_spot + assign handles 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 vehicle field is atomic. find_spot picks a candidate; assign does 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. ParkingLot becomes 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, Ticket cleanly?
  • Did you avoid putting "find a spot" logic inside ParkingLot directly (vs. delegating to Floor)?
  • 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?
Found this useful?