LLD ยท 3 min read

Low-Level Design series #3

Design a Parking Lot

Classic LLD problem: designing a parking lot system.

On this page
  1. Overview (Problem statement)
  2. Entities & Classes
  3. 1. Vehicle (Base Class + Subtypes)
  4. 2. Spot Class
  5. 3. Floor Class
  6. 4. ParkingLot Class
  7. 5. BillingService Class
  8. Class Relationship Summary
  9. Key design decisions that were imp here

Overview (Problem statement)

Design the classes and relationships for a parking lot that:

  • Has multiple floors
  • Supports different vehicle types (bike, car, truck)
  • Can tell you if a spot is available
  • Can assign a spot to a vehicle and release it

Entities & Classes

1. Vehicle (Base Class + Subtypes)

Rather than completely separate unrelated classes, we use inheritance. All vehicles share common attributes, but type matters for spot assignment.

java
public enum VehicleType {
    BIKE, CAR, TRUCK
}

public abstract class Vehicle {
    private String licensePlate;
    private VehicleType type;

    public Vehicle(String licensePlate, VehicleType type) {
        this.licensePlate = licensePlate;
        this.type = type;
    }

    public VehicleType getType() { return type; }
    public String getLicensePlate() { return licensePlate; }
}

public class Car extends Vehicle {
    public Car(String licensePlate) {
        super(licensePlate, VehicleType.CAR);
    }
}

public class Bike extends Vehicle {
    public Bike(String licensePlate) {
        super(licensePlate, VehicleType.BIKE);
    }
}

public class Truck extends Vehicle {
    public Truck(String licensePlate) {
        super(licensePlate, VehicleType.TRUCK);
    }
}

2. Spot Class

A spot knows its own ID, which vehicle type it supports, and when it was occupied.

java
public class Spot implements Comparable<Spot> {
    private String spotId;
    private VehicleType supportedType;
    private Vehicle currentVehicle;
    private LocalDateTime entryTime;
    private int floorNumber;
    private double pricePerHour;

    public Spot(String spotId, VehicleType supportedType, int floorNumber, double pricePerHour) {
        this.spotId = spotId;
        this.supportedType = supportedType;
        this.floorNumber = floorNumber;
        this.pricePerHour = pricePerHour;
    }

    public boolean isAvailable() {
        return currentVehicle == null;
    }

    public void assignVehicle(Vehicle v) {
        this.currentVehicle = v;
        this.entryTime = LocalDateTime.now();
    }

    public void removeVehicle() {
        this.currentVehicle = null;
        this.entryTime = null;
    }

    public LocalDateTime getEntryTime() { return entryTime; }
    public Vehicle getCurrentVehicle() { return currentVehicle; }
    public String getSpotId() { return spotId; }
    public int getFloorNumber() { return floorNumber; }
    public double getPricePerHour() { return pricePerHour; }
    public VehicleType getSupportedType() { return supportedType; }

    @Override
    public int compareTo(Spot other) {
        return Integer.compare(this.floorNumber, other.floorNumber);
    }
}

3. Floor Class

A floor has spots. That's it. It doesn't manage availability. That's the ParkingLot's job.

java
public class Floor {
    private int floorNumber;
    private List<Spot> spots;

    public Floor(int floorNumber) {
        this.floorNumber = floorNumber;
        this.spots = new ArrayList<>();
    }

    public void addSpot(Spot spot) {
        spots.add(spot);
    }

    public List<Spot> getSpots() { return spots; }
    public int getFloorNumber() { return floorNumber; }
}

4. ParkingLot Class

The ParkingLot is a container for floors, but also owns the availability tracking. It uses:

java
Map<VehicleType, PriorityQueue<Spot>> availableSpots

The PriorityQueue orders spots by floor number, so the nearest floor spot is always assigned first : O(log n) fetch, O(log n) reinsert.

java
public class ParkingLot {
    private String name;
    private List<Floor> floors;
    private Map<VehicleType, PriorityQueue<Spot>> availableSpots;

    public ParkingLot(String name) {
        this.name = name;
        this.floors = new ArrayList<>();
        this.availableSpots = new HashMap<>();

        for (VehicleType type : VehicleType.values()) {
            availableSpots.put(type, new PriorityQueue<>());
        }
    }

    public void addFloor(Floor floor) {
        floors.add(floor);
        for (Spot spot : floor.getSpots()) {
            availableSpots.get(spot.getSupportedType()).offer(spot);
        }
    }

    public boolean hasAvailableSpot(VehicleType type) {
        return !availableSpots.get(type).isEmpty();
    }

    public Spot assignSpot(Vehicle vehicle) {
        PriorityQueue<Spot> queue = availableSpots.get(vehicle.getType());

        if (queue == null || queue.isEmpty()) {
            throw new RuntimeException("No available spot for vehicle type: " + vehicle.getType());
        }

        Spot spot = queue.poll();
        spot.assignVehicle(vehicle);
        return spot;
    }

    public double releaseSpot(Spot spot) {
        if (spot.isAvailable()) {
            throw new IllegalStateException("Spot is already empty: " + spot.getSpotId());
        }

        LocalDateTime exitTime = LocalDateTime.now();
        double bill = BillingService.calculateBill(spot, exitTime);
        spot.removeVehicle();
        availableSpots.get(spot.getSupportedType()).offer(spot);
        return bill;
    }
}

5. BillingService Class

Single responsibility i.e. just calculates the bill. Takes a spot (which has entry time and price per hour) and an exit time.

java
public class BillingService {

    public static double calculateBill(Spot spot, LocalDateTime exitTime) {
        LocalDateTime entryTime = spot.getEntryTime();
        long minutes = Duration.between(entryTime, exitTime).toMinutes();
        double hours = Math.ceil(minutes / 60.0);
        return hours * spot.getPricePerHour();
    }
}

Class Relationship Summary

mermaid
flowchart TD
  ParkingLot -->|has many| Floor
  Floor -->|has many| Spot
  Spot -->|may or may not contain| Vehicle
  ParkingLot -->|owns| Availability["Map#lt;VehicleType, PriorityQueue#lt;Spot#gt;#gt;<br/>availability tracking"]
  BillingService["BillingService (stateless)"] -->|"uses (time + price)"| Spot

Aggregation vs Composition:

  • ParkingLot -> Floor: Composition (floors don't exist without the lot)
  • Floor -> Spot: Composition
  • Spot -> Vehicle: Aggregation (vehicle exists independently, just parked here)

Key design decisions that were imp here

  • Base Vehicle class: Avoids duplication, type is just an enum field
  • Spot implements Comparable: Enables PriorityQueue to sort by floor number natively
  • Map<VehicleType, PriorityQueue<Spot>>: O(1) type lookup, O(log n) spot assignment which is way better than O(n) scan
  • BillingService as a separate class: Single Responsibility as billing logic doesn't belong on Spot or ParkingLot
  • Floor as a dumb container: Floor doesn't manage availability, keeps it simple and focused