Design a Parking Lot

The classic object-modeling round. How to scope it, the entities and their relationships, where Strategy fits for spot assignment, and the extensibility follow-ups.

LLDOODclass-design

The analogy: a physical slot allocator

Think of a parking lot like a physical mail room in a massive skyscraper. Mail slots (parking spots) come in different sizes: small slots for letters (motorcycles), medium slots for packages (cars), and large bins for crates (buses).

The clerk at the front desk (the ParkingLot system) has to allocate these slots. If a package (car) arrives, the clerk looks at the floor plans (levels) to find the nearest open slot of the correct size. Once allocated, a receipt (ticket) is issued. If two clerks try to assign the same slot at the exact same moment (concurrency race), they might write over each other, leaving two packages jammed in one box.

Our job is to design the classes, their relationships, and the allocation policies to make this system durable, extensible, and thread-safe.

How to run an OOD question

It's not about UML perfection — it's about clean responsibilities and extensibility. Steps: clarify scope → identify entities (nouns) → assign responsibilities (verbs) → relationships → call out where behavior varies (Strategy) → walk a scenario.

Clarify first: multiple levels? vehicle sizes (motorcycle/car/bus)? pricing? payment? Pin scope: "single lot, multiple levels, three spot sizes, hourly pricing, ignore payments — OK?"

Entities & relationships

  • ParkingLot is the facade: parkVehicle / unpark. It owns levels and issues tickets.
  • Level owns spots and answers findSpot(vehicle).
  • ParkingSpot has a size and canFit(vehicle); subclasses or a size enum (prefer the enum + canFit over a deep spot hierarchy).
  • Vehicle hierarchy (Car/Motorcycle/Bus) carries size.
  • Ticket records spot + entry time for fee calculation.

Where the patterns live

  • Strategy — spot assignment. "nearest", "first-available", "fit smallest spot" are interchangeable SpotAssignmentStrategy implementations injected into Level. New policy = new class, no edits (Open/Closed).
  • Strategy — pricing. FeeStrategy (flat, hourly, tiered) computed from the ticket.
  • Singleton (maybe). One ParkingLot instance — but inject it rather than reaching for a global.
Prefer an enum + capability over a class explosion

Modeling every spot/vehicle combination as a subclass blows up fast. A size enum plus spot.canFit(vehicle) keeps it small and handles "a car can use a large spot if no compact is free" without new types.

Here's that spot-assignment strategy running. A motorcycle fits anywhere; a car needs a compact-or-larger spot; a large vehicle needs a large spot. Watch the scan reject spots that are taken or too small, then take the first that fits — swap the operations to test "two cars, one compact left".

Parking lot — nearest-fit spot assignmenttime O(spots) per parkspace O(spots)
entrance →
moto·spot 0
moto·spot 1
compact·spot 2
compact·spot 3
compact·spot 4
large·spot 5
large·spot 6

dashed = free · a vehicle takes the nearest spot ranked its size or larger

1/287 spots: two motorcycle (M), three compact (C), two large (L). A vehicle parks in the nearest spot it fits — bigger spots accept smaller vehicles, never the reverse.

free = 7

Think it through like the interview

Before reading further, try to run the method yourself — this widget walks the exact five moves, one at a time:

Think it through: Design a Parking LotLLD Classic0/5 stages

PROBLEMDesign the classes for a parking lot: vehicles arrive, get a spot that fits, receive a ticket, and pay on exit. You have ~35 minutes and a whiteboard.

  1. 1

    Clarify scope

    What would change my design the most? Ask THOSE questions first.

  2. 2

    Nouns → entities

    Read the problem statement back. Which nouns survive as classes?

    unlocks after the stage above
  3. 3

    Verbs → responsibilities

    Who owns parkVehicle? Who owns findSpot? Why not all on ParkingLot?

    unlocks after the stage above
  4. 4

    Spot what varies → patterns

    Which decisions might change next month without the structure changing?

    unlocks after the stage above
  5. 5

    Walk a scenario + break it

    Narrate 'a car arrives'. Then: two cars, one spot left. What breaks?

    unlocks after the stage above

Walk a scenario + concurrency

"A car arrives": ParkingLot.parkVehicle(car) → ask each Level for a spot via its assignment strategy → mark spot occupied → issue Ticket. Concurrency follow-up: two cars racing for the last spot is the same race as a balance double-spend — guard spot acquisition atomically (compare-and-set on the spot's state, or a lock per level) so a spot is never double-assigned.

Code Implementation

Below are working class templates for a thread-safe Parking Lot in Python, Java, and C++:

1. Python

Python
from abc import ABC, abstractmethod
from enum import Enum
import threading
from typing import List, Optional

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

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

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

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

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

class ParkingSpot:
    def __init__(self, spot_number: int, size: VehicleSize):
        self.spot_number = spot_number
        self.size = size
        self.vehicle: Optional[Vehicle] = None

    def is_free(self) -> bool:
        return self.vehicle is None

    def can_fit(self, vehicle: Vehicle) -> bool:
        return self.is_free() and self.size.value >= vehicle.size.value

    def park(self, vehicle: Vehicle) -> bool:
        if not self.can_fit(vehicle):
            return False
        self.vehicle = vehicle
        return True

    def remove_vehicle(self):
        self.vehicle = None

class SpotAssignmentStrategy(ABC):
    @abstractmethod
    def find_spot(self, spots: List[ParkingSpot], vehicle: Vehicle) -> Optional[ParkingSpot]:
        pass

class FirstAvailableStrategy(SpotAssignmentStrategy):
    def find_spot(self, spots: List[ParkingSpot], vehicle: Vehicle) -> Optional[ParkingSpot]:
        for spot in spots:
            if spot.can_fit(vehicle):
                return spot
        return None

class Level:
    def __init__(self, floor_number: int, spots: List[ParkingSpot], strategy: SpotAssignmentStrategy):
        self.floor_number = floor_number
        self.spots = spots
        self.strategy = strategy
        self._lock = threading.Lock() # Lock level to prevent reservation races

    def park_vehicle(self, vehicle: Vehicle) -> bool:
        with self._lock: # Atomic check-and-reserve
            spot = self.strategy.find_spot(self.spots, vehicle)
            if spot:
                return spot.park(vehicle)
            return False

    def unpark_vehicle(self, vehicle: Vehicle) -> bool:
        with self._lock:
            for spot in self.spots:
                if spot.vehicle == vehicle:
                    spot.remove_vehicle()
                    return True
            return False

2. Java

Java
import java.util.List;
import java.util.Optional;
import java.util.concurrent.locks.ReentrantLock;

enum VehicleSize {
    MOTORCYCLE(1), COMPACT(2), LARGE(3);
    private final int value;
    VehicleSize(int val) { this.value = val; }
    public int getValue() { return value; }
}

abstract class Vehicle {
    private final VehicleSize size;
    private final String licensePlate;
    public Vehicle(VehicleSize size, String lp) { this.size = size; this.licensePlate = lp; }
    public VehicleSize getSize() { return size; }
}

class Car extends Vehicle {
    public Car(String lp) { super(VehicleSize.COMPACT, lp); }
}

class ParkingSpot {
    private final int spotNumber;
    private final VehicleSize size;
    private Vehicle vehicle;

    public ParkingSpot(int spotNumber, VehicleSize size) {
        this.spotNumber = spotNumber;
        this.size = size;
    }

    public synchronized boolean isFree() { return vehicle == null; }
    
    public synchronized boolean canFit(Vehicle v) {
        return isFree() && size.getValue() >= v.getSize().getValue();
    }

    public synchronized boolean park(Vehicle v) {
        if (!canFit(v)) return false;
        this.vehicle = v;
        return true;
    }

    public synchronized void removeVehicle() { this.vehicle = null; }
    public synchronized Vehicle getVehicle() { return vehicle; }
}

interface SpotAssignmentStrategy {
    Optional<ParkingSpot> findSpot(List<ParkingSpot> spots, Vehicle vehicle);
}

class FirstAvailableStrategy implements SpotAssignmentStrategy {
    @Override
    public Optional<ParkingSpot> findSpot(List<ParkingSpot> spots, Vehicle v) {
        return spots.stream().filter(spot -> spot.canFit(v)).findFirst();
    }
}

class Level {
    private final int floorNumber;
    private final List<ParkingSpot> spots;
    private final SpotAssignmentStrategy strategy;
    private final ReentrantLock lock = new ReentrantLock();

    public Level(int floorNumber, List<ParkingSpot> spots, SpotAssignmentStrategy strat) {
        this.floorNumber = floorNumber;
        this.spots = spots;
        this.strategy = strat;
    }

    public boolean parkVehicle(Vehicle v) {
        lock.lock();
        try {
            Optional<ParkingSpot> spot = strategy.findSpot(spots, v);
            if (spot.isPresent()) {
                return spot.get().park(v);
            }
            return false;
        } finally {
            lock.unlock();
        }
    }
}

3. C++

C++
#include <vector>
#include <string>
#include <memory>
#include <mutex>
#include <algorithm>

enum class VehicleSize { MOTORCYCLE = 1, COMPACT = 2, LARGE = 3 };

class Vehicle {
private:
    VehicleSize size;
    std::string license_plate;
protected:
    Vehicle(VehicleSize sz, std::string lp) : size(sz), license_plate(lp) {}
public:
    virtual ~Vehicle() = default;
    VehicleSize getSize() const { return size; }
};

class Car : public Vehicle {
public:
    Car(std::string lp) : Vehicle(VehicleSize::COMPACT, lp) {}
};

class ParkingSpot {
private:
    int spot_number;
    VehicleSize size;
    std::shared_ptr<Vehicle> vehicle;
    std::mutex spot_mutex;
public:
    ParkingSpot(int num, VehicleSize sz) : spot_number(num), size(sz), vehicle(nullptr) {}
    
    bool isFree() {
        std::lock_guard<std::mutex> lock(spot_mutex);
        return vehicle == nullptr;
    }
    
    bool canFit(const Vehicle& v) {
        std::lock_guard<std::mutex> lock(spot_mutex);
        return vehicle == nullptr && static_cast<int>(size) >= static_cast<int>(v.getSize());
    }
    
    bool park(std::shared_ptr<Vehicle> v) {
        std::lock_guard<std::mutex> lock(spot_mutex);
        if (vehicle != nullptr || static_cast<int>(size) < static_cast<int>(v->getSize())) {
            return false;
        }
        vehicle = v;
        return true;
    }
    
    void removeVehicle() {
        std::lock_guard<std::mutex> lock(spot_mutex);
        vehicle = nullptr;
    }
};

class SpotAssignmentStrategy {
public:
    virtual ~SpotAssignmentStrategy() = default;
    virtual std::shared_ptr<ParkingSpot> findSpot(const std::vector<std::shared_ptr<ParkingSpot>>& spots, const Vehicle& vehicle) = 0;
};

class FirstAvailableStrategy : public SpotAssignmentStrategy {
public:
    std::shared_ptr<ParkingSpot> findSpot(const std::vector<std::shared_ptr<ParkingSpot>>& spots, const Vehicle& vehicle) override {
        for (auto spot : spots) {
            if (spot->canFit(vehicle)) return spot;
        }
        return nullptr;
    }
};

class Level {
private:
    int floor_number;
    std::vector<std::shared_ptr<ParkingSpot>> spots;
    std::shared_ptr<SpotAssignmentStrategy> strategy;
    std::mutex level_mutex;
public:
    Level(int num, std::vector<std::shared_ptr<ParkingSpot>> s, std::shared_ptr<SpotAssignmentStrategy> strat) 
        : floor_number(num), spots(s), strategy(strat) {}
        
    bool parkVehicle(std::shared_ptr<Vehicle> v) {
        std::lock_guard<std::mutex> lock(level_mutex); // Lock level to serialize search + reserve
        auto spot = strategy->findSpot(spots, *v);
        if (spot) {
            return spot->park(v);
        }
        return false;
    }
};

Check yourself

Check yourself0/3 answered

1. Why do we place the Lock/Mutex at the Level level instead of inside the ParkingSpot class during the find-and-reserve operation?

2. How does using the Strategy pattern for spot assignment satisfy the SOLID principles?

3. Why is a VehicleSize enum preferred over creating a deep subclass hierarchy for every combination of spot and vehicle type (e.g. MotorcycleSpot, CompactSpot, LargeSpot)?

Practice — level up

A parking lot is an allocation problem: hand out a limited resource, take it back, and enforce "one holder at a time". These drills are the same shape.

Practice ladder: Resource allocation0/4 solved

Climb in order — every rung assumes the one above it. Solve on LeetCode, then tick it here; progress is saved on this device.

Warm-up — count what's free

Allocation with nothing but counters.
  1. Per-size counters — the simplest spot allocation.

Core — allocate & release specific slots

Now identity matters: which spot, given back.
  1. Reserve / unreserve numbered slots — parking spots by another name.

  2. Fixed capacity with free/occupied bookkeeping and wrap-around.

Stretch — paired entry/exit + fees

Ticket on the way in, charge on the way out.
  1. Check-in/check-out pairs and aggregate on exit — the ticket-and-fee half of the lot.