Low-Level Design: Parking Lot System (OOP Interview)

Problem Statement

Design an object-oriented parking lot system. The lot has multiple levels, each with multiple spots of different sizes (motorcycle, compact, large). Vehicles of various sizes (motorcycle, car, truck) enter, park in the first available suitable spot, and exit with a fee calculated based on parked duration.

Class Hierarchy

from enum import Enum
from dataclasses import dataclass, field
from datetime import datetime
from typing import Optional
import threading

class VehicleSize(Enum):
    MOTORCYCLE = 1
    COMPACT    = 2
    LARGE      = 3

class SpotSize(Enum):
    MOTORCYCLE = 1
    COMPACT    = 2
    LARGE      = 3

# Vehicle hierarchy
class Vehicle:
    def __init__(self, license_plate: str, size: VehicleSize):
        self.license_plate = license_plate
        self.size = size

    def can_fit_in(self, spot_size: SpotSize) -> bool:
        # Vehicle fits if spot is >= vehicle size
        return spot_size.value >= self.size.value

class Motorcycle(Vehicle):
    def __init__(self, license_plate: str):
        super().__init__(license_plate, VehicleSize.MOTORCYCLE)

class Car(Vehicle):
    def __init__(self, license_plate: str):
        super().__init__(license_plate, VehicleSize.COMPACT)

class Truck(Vehicle):
    def __init__(self, license_plate: str):
        super().__init__(license_plate, VehicleSize.LARGE)

Spot and Ticket

class ParkingSpot:
    def __init__(self, level: int, spot_id: int, size: SpotSize):
        self.level    = level
        self.spot_id  = spot_id
        self.size     = size
        self.vehicle: Optional[Vehicle] = None

    @property
    def is_available(self) -> bool:
        return self.vehicle is None

    def park(self, vehicle: Vehicle) -> bool:
        if not self.is_available or not vehicle.can_fit_in(self.size):
            return False
        self.vehicle = vehicle
        return True

    def remove_vehicle(self) -> Optional[Vehicle]:
        v = self.vehicle
        self.vehicle = None
        return v

@dataclass
class Ticket:
    ticket_id:   str
    vehicle:     Vehicle
    spot:        ParkingSpot
    entry_time:  datetime = field(default_factory=datetime.now)
    exit_time:   Optional[datetime] = None

    def calculate_fee(self, hourly_rates: dict[VehicleSize, float]) -> float:
        if self.exit_time is None:
            self.exit_time = datetime.now()
        duration_hours = (self.exit_time - self.entry_time).total_seconds() / 3600
        rate = hourly_rates.get(self.vehicle.size, 5.0)
        return round(duration_hours * rate, 2)

Level and Parking Lot

class Level:
    def __init__(self, level_id: int, spots_config: dict[SpotSize, int]):
        # spots_config = {SpotSize.MOTORCYCLE: 20, SpotSize.COMPACT: 30, SpotSize.LARGE: 10}
        self.level_id = level_id
        self.spots: list[ParkingSpot] = []
        spot_id = 0
        for size, count in spots_config.items():
            for _ in range(count):
                self.spots.append(ParkingSpot(level_id, spot_id, size))
                spot_id += 1

    def find_available_spot(self, vehicle: Vehicle) -> Optional[ParkingSpot]:
        for spot in self.spots:
            if spot.is_available and vehicle.can_fit_in(spot.size):
                return spot
        return None

    def available_count(self, size: SpotSize) -> int:
        return sum(1 for s in self.spots if s.size == size and s.is_available)


class ParkingLot:
    _instance = None
    _lock = threading.Lock()

    def __new__(cls):
        with cls._lock:
            if cls._instance is None:
                cls._instance = super().__new__(cls)
                cls._instance._initialized = False
        return cls._instance

    def __init__(self):
        if self._initialized:
            return
        self.levels: list[Level] = []
        self.active_tickets: dict[str, Ticket] = {}  # license_plate -> Ticket
        self.hourly_rates = {
            VehicleSize.MOTORCYCLE: 2.0,
            VehicleSize.COMPACT:    4.0,
            VehicleSize.LARGE:      6.0,
        }
        self._ticket_counter = 0
        self._lock = threading.Lock()
        self._initialized = True

    def add_level(self, level: Level):
        self.levels.append(level)

    def _generate_ticket_id(self) -> str:
        self._ticket_counter += 1
        return f"TKT-{self._ticket_counter:06d}"

    def park_vehicle(self, vehicle: Vehicle) -> Optional[Ticket]:
        with self._lock:
            if vehicle.license_plate in self.active_tickets:
                print(f"{vehicle.license_plate} already parked.")
                return None

            for level in self.levels:
                spot = level.find_available_spot(vehicle)
                if spot:
                    spot.park(vehicle)
                    ticket = Ticket(
                        ticket_id=self._generate_ticket_id(),
                        vehicle=vehicle,
                        spot=spot,
                    )
                    self.active_tickets[vehicle.license_plate] = ticket
                    print(f"Parked {vehicle.license_plate} at level {spot.level}, "
                          f"spot {spot.spot_id} ({spot.size.name})")
                    return ticket

            print(f"No available spot for {vehicle.license_plate}")
            return None

    def exit_vehicle(self, license_plate: str) -> float:
        with self._lock:
            ticket = self.active_tickets.pop(license_plate, None)
            if not ticket:
                print(f"No active ticket for {license_plate}")
                return 0.0
            ticket.exit_time = datetime.now()
            ticket.spot.remove_vehicle()
            fee = ticket.calculate_fee(self.hourly_rates)
            print(f"Vehicle {license_plate} exited. Fee: $" + f"{fee:.2f}")
            return fee

    def get_availability(self) -> dict:
        with self._lock:
            result = {}
            for level in self.levels:
                result[f"Level {level.level_id}"] = {
                    size.name: level.available_count(size)
                    for size in SpotSize
                }
            return result

Usage Example

def demo():
    lot = ParkingLot()

    # Configure two levels
    lot.add_level(Level(0, {SpotSize.MOTORCYCLE: 5, SpotSize.COMPACT: 10, SpotSize.LARGE: 3}))
    lot.add_level(Level(1, {SpotSize.MOTORCYCLE: 5, SpotSize.COMPACT: 10, SpotSize.LARGE: 3}))

    # Vehicles arrive
    bike  = Motorcycle("MOTO-001")
    car1  = Car("CAR-001")
    truck = Truck("TRUCK-001")

    lot.park_vehicle(bike)
    lot.park_vehicle(car1)
    lot.park_vehicle(truck)

    print(lot.get_availability())

    # Vehicles exit
    lot.exit_vehicle("CAR-001")
    lot.exit_vehicle("TRUCK-001")

demo()

Design Decisions and Trade-offs

  • Singleton ParkingLot: Thread-safe double-checked locking ensures one lot instance. In a microservice context, replace with a stateless service backed by a database.
  • Vehicle fits in larger spot: can_fit_in compares enum values numerically — a motorcycle (size=1) fits in a compact (size=2) or large (size=3) spot. This maximizes utilization at the cost of leaving large spots for trucks.
  • Spot selection strategy: The current implementation takes the first available spot (top-to-bottom). Production improvement: prefer the smallest suitable spot (don’t park a motorcycle in a large spot unless necessary). Implement by iterating spots sorted by size.
  • Thread safety: A single threading.Lock guards park_vehicle and exit_vehicle. For higher concurrency: use per-level locks, or (better) a database with row-level locking and optimistic concurrency.
  • Fee calculation: Simple hourly rate. Extensions: minimum charge (first 30 minutes free), daily maximums, validation discounts.

Extension: Reservation System

@dataclass
class Reservation:
    reservation_id: str
    vehicle_size:   VehicleSize
    start_time:     datetime
    end_time:       datetime
    spot:           Optional[ParkingSpot] = None

    def is_active(self) -> bool:
        now = datetime.now()
        return self.start_time <= now <= self.end_time

# ParkingLot.reserve_spot() would:
# 1. Find a spot not reserved for the given time window
# 2. Create a Reservation record
# 3. Return reservation_id
# On arrival: look up reservation_id, assign the pre-reserved spot

Interview Checklist

  • Clarify: number of levels, spot sizes, vehicle types, fee structure
  • Inheritance: Vehicle and Spot hierarchies with size enum comparison
  • Thread safety: lock on park/exit; discuss per-level lock for scale
  • Singleton: ParkingLot as singleton; mention stateless alternative for microservices
  • Spot selection: first-fit vs best-fit; prefer smallest fitting spot
  • Fee: time-based rate per vehicle size; handle edge cases (minimum fee, overstay)
  • Extensibility: reservation system, EV charging spots, monthly passes

{“@context”:”https://schema.org”,”@type”:”FAQPage”,”mainEntity”:[{“@type”:”Question”,”name”:”How do you design a parking lot system for an OOP interview?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Core classes: (1) VehicleSize and SpotSize enums with integer values (MOTORCYCLE=1, COMPACT=2, LARGE=3). A vehicle fits in a spot if spot_size.value >= vehicle_size.value — enabling motorcycles to park in compact or large spots. (2) Vehicle ABC with subclasses Motorcycle, Car, Truck — each sets its size in the constructor. can_fit_in(spot_size) centralizes the size-compatibility logic. (3) ParkingSpot stores level, spot_id, size, and the parked vehicle (None if empty). park(vehicle) and remove_vehicle() are the only mutators. (4) Level contains a list of ParkingSpot objects and a find_available_spot(vehicle) method that scans for the first available spot the vehicle can fit in. (5) ParkingLot (Singleton) manages levels, active_tickets dict (license_plate → Ticket), and thread-safe park_vehicle / exit_vehicle methods. (6) Ticket records vehicle, spot, entry_time, and computes fee on exit using hourly rates per vehicle size. The Singleton pattern ensures all application code references the same lot state.”}},{“@type”:”Question”,”name”:”How do you make a parking lot system thread-safe?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”The critical section is the park_vehicle operation: check if vehicle is already parked, scan for an available spot, mark the spot as occupied, create a ticket — all of these steps must be atomic. Without a lock, two threads could both find the same empty spot and both try to park there (TOCTOU race condition). Solution: wrap park_vehicle and exit_vehicle in a threading.Lock (or asyncio.Lock for async frameworks). The lock ensures only one vehicle enters or exits at a time. For higher throughput: use per-level locks. Since vehicles on level 0 and level 1 don’t share spots, their operations don’t need to coordinate. Each Level gets its own lock; ParkingLot acquires the level lock when calling find_available_spot and park. This reduces contention from O(all_vehicles) to O(vehicles_per_level). For a distributed parking lot (multiple buildings, central reservation): use a database with row-level locking (SELECT FOR UPDATE on the spot row) and optimistic concurrency (version column). The application-level lock is replaced by database transactions with retry on conflict.”}},{“@type”:”Question”,”name”:”How do you choose the best parking spot — first-fit vs best-fit?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”First-fit: return the first available spot the vehicle can fit in (scanning spots in order). Simple O(n) scan. Problem: a motorcycle might always be assigned a large spot if large spots appear first in the list — wasting large spots for trucks. Best-fit: return the smallest available spot that fits the vehicle. A motorcycle gets a motorcycle spot if one is available; only uses a compact or large spot when motorcycle spots are full. Implementation: group spots by size in separate lists (dict[SpotSize, deque]). To find a spot for a vehicle of size S: try SpotSize.MOTORCYCLE first if S == MOTORCYCLE, then SpotSize.COMPACT, then SpotSize.LARGE. Pop from the front of the matching deque; push back on exit. This is O(1) per operation instead of O(n) scan. Alternatively, maintain available counts per size and a priority queue of free spot IDs per size. Best-fit maximizes utilization of small spots and reserves large spots for large vehicles. In an interview, propose first-fit first (simpler to code), then mention best-fit as an optimization.”}}]}

🏢 Asked at: Uber Interview Guide 2026: Dispatch Systems, Geospatial Algorithms, and Marketplace Engineering

🏢 Asked at: Lyft Interview Guide 2026: Rideshare Engineering, Real-Time Dispatch, and Safety Systems

🏢 Asked at: Atlassian Interview Guide

🏢 Asked at: Shopify Interview Guide

🏢 Asked at: DoorDash Interview Guide

Scroll to Top