All Posts

Implement LLD for Parking Lot: Code Walkthrough

Theory is good, code is better. We implement the Parking Lot system in Java, focusing on the Singleton, Factory, and Strategy patterns.

Abstract AlgorithmsAbstract Algorithms
ยทยท6 min read
Share
Share on X / Twitter
Share on LinkedIn
Copy link

TLDR: This is the code companion to the Parking Lot System Design post. We implement the core classes (ParkingLot, ParkingSpot, Ticket) in Java, apply the Singleton, Factory, and Strategy patterns, and use a Min-Heap to find the nearest available spot in O(log n).


๐Ÿ“– Design-to-Code: What We Are Building

In the design post we identified key requirements:

  • Support multiple vehicle types: CAR, TRUCK, BIKE.
  • Multiple floors, each with typed spots.
  • A Ticket issued on entry; freed on exit with fee calculated.
  • Find the nearest available spot for the vehicle's type.
  • Thread-safe: multiple entry gates run concurrently.

Here we translate each requirement into Java classes with production-grade patterns.


๐Ÿ”ข The Domain Model: Vehicles, Spots, and Tickets

Vehicle Hierarchy

public enum VehicleType { BIKE, CAR, TRUCK }

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

    protected 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 plate)   { super(plate, VehicleType.CAR); } }
public class Truck  extends Vehicle { public Truck(String plate) { super(plate, VehicleType.TRUCK); } }
public class Bike   extends Vehicle { public Bike(String plate)  { super(plate, VehicleType.BIKE); } }

Parking Spot Hierarchy

public abstract class ParkingSpot {
    private final int id;
    private volatile boolean isFree = true;
    private final VehicleType type;
    private Vehicle parkedVehicle;

    protected ParkingSpot(int id, VehicleType type) {
        this.id = id;
        this.type = type;
    }

    public synchronized boolean assign(Vehicle v) {
        if (!isFree) return false;
        this.parkedVehicle = v;
        this.isFree = false;
        return true;
    }

    public synchronized void free() {
        this.parkedVehicle = null;
        this.isFree = true;
    }

    public boolean isFree() { return isFree; }
    public int getId()       { return id; }
    public VehicleType getType() { return type; }
}

public class BikeSpot    extends ParkingSpot { public BikeSpot(int id)    { super(id, VehicleType.BIKE); } }
public class CompactSpot extends ParkingSpot { public CompactSpot(int id) { super(id, VehicleType.CAR); } }
public class LargeSpot   extends ParkingSpot { public LargeSpot(int id)   { super(id, VehicleType.TRUCK); } }

โš™๏ธ ParkingFloor: Managing Spots via a Min-Heap

Each floor maintains a Min-Heap per vehicle type. The heap is ordered by spot ID โ€” the smallest ID = nearest to the entrance.

Why Min-Heap?

  • findNearest() โ†’ O(1) peek, O(log n) remove.
  • freeSpot() โ†’ O(log n) re-insert.
  • Outperforms linear scan O(n) at scale.
import java.util.PriorityQueue;
import java.util.Map;
import java.util.HashMap;

public class ParkingFloor {
    private final int floorNumber;
    private final Map<VehicleType, PriorityQueue<ParkingSpot>> availableSpots;

    public ParkingFloor(int floorNumber) {
        this.floorNumber = floorNumber;
        availableSpots = new HashMap<>();
        for (VehicleType type : VehicleType.values()) {
            availableSpots.put(type,
                new PriorityQueue<>((a, b) -> a.getId() - b.getId()));
        }
    }

    public synchronized ParkingSpot findAndAssign(Vehicle v) {
        PriorityQueue<ParkingSpot> heap = availableSpots.get(v.getType());
        if (heap.isEmpty()) return null;
        ParkingSpot spot = heap.poll();   // O(log n)
        spot.assign(v);
        return spot;
    }

    public synchronized void releaseSpot(ParkingSpot spot) {
        spot.free();
        availableSpots.get(spot.getType()).offer(spot);   // O(log n)
    }

    public void addSpot(ParkingSpot spot) {
        availableSpots.get(spot.getType()).offer(spot);
    }
}
flowchart TD
    Vehicle["Vehicle.getType() = CAR"] --> Floor["ParkingFloor.findAndAssign()"]
    Floor --> Heap["MinHeap[CompactSpot]\npeek โ†’ Spot #3"]
    Heap -->|poll O(log n)| Spot["Spot #3: assign(vehicle)"]
    Spot --> Ticket["Issue Ticket(spotId=3, entryTime=now)"]

import java.time.Instant;
import java.util.List;
import java.util.ArrayList;

public class ParkingLot {
    private static volatile ParkingLot instance;
    private final List<ParkingFloor> floors = new ArrayList<>();

    private ParkingLot() {}

    public static ParkingLot getInstance() {
        if (instance == null) {
            synchronized (ParkingLot.class) {
                if (instance == null) {           // double-checked locking
                    instance = new ParkingLot();
                }
            }
        }
        return instance;
    }

    public void addFloor(ParkingFloor floor) {
        floors.add(floor);
    }

    public Ticket park(Vehicle v) {
        for (ParkingFloor floor : floors) {
            ParkingSpot spot = floor.findAndAssign(v);
            if (spot != null) {
                return new Ticket(v, spot, floor, Instant.now());
            }
        }
        throw new RuntimeException("Parking lot full for type: " + v.getType());
    }

    public double exit(Ticket ticket) {
        Instant exitTime = Instant.now();
        ticket.getFloor().releaseSpot(ticket.getSpot());
        long minutes = (exitTime.getEpochSecond() - ticket.getEntryTime().getEpochSecond()) / 60;
        return FeeCalculator.calculate(ticket.getVehicle().getType(), minutes);
    }
}

Double-checked locking: The volatile keyword on instance prevents instruction reordering. Both null checks prevent creating two instances under race conditions at startup.


โš™๏ธ Ticket and Fee Strategy Pattern

public class Ticket {
    private final Vehicle vehicle;
    private final ParkingSpot spot;
    private final ParkingFloor floor;
    private final Instant entryTime;

    public Ticket(Vehicle vehicle, ParkingSpot spot, ParkingFloor floor, Instant entryTime) {
        this.vehicle = vehicle;
        this.spot = spot;
        this.floor = floor;
        this.entryTime = entryTime;
    }

    // Getters...
}
public class FeeCalculator {
    private static final Map<VehicleType, Double> RATE_PER_HOUR = Map.of(
        VehicleType.BIKE,  1.0,
        VehicleType.CAR,   2.0,
        VehicleType.TRUCK, 3.5
    );

    public static double calculate(VehicleType type, long minutes) {
        double hours = Math.ceil(minutes / 60.0);   // round up to next hour
        return RATE_PER_HOUR.getOrDefault(type, 2.0) * hours;
    }
}

Using a Strategy pattern here (even as simple as a rate map) makes it easy to swap pricing logic (dynamic pricing, peak hours) without touching ParkingLot or Ticket.


โš–๏ธ Design Patterns Applied

PatternWhere UsedWhy
SingletonParkingLot.getInstance()Single source of truth for lot state
Min-HeapParkingFloor.availableSpotsO(log n) nearest-spot allocation
StrategyFeeCalculatorSwap pricing logic independently
Factory MethodVehicleFactory.create(type, plate)Decouple creation from usage
Template MethodParkingSpot.assign() / free()Common lifecycle in abstract base

๐Ÿงช Testing the Critical Path

@Test
void carParksThenExits() {
    ParkingLot lot = ParkingLot.getInstance();
    ParkingFloor floor = new ParkingFloor(1);
    floor.addSpot(new CompactSpot(1));
    floor.addSpot(new CompactSpot(2));
    lot.addFloor(floor);

    Vehicle car = new Car("ABC-123");
    Ticket ticket = lot.park(car);

    assertEquals(1, ticket.getSpot().getId());  // nearest spot
    assertTrue(!ticket.getSpot().isFree());

    double fee = lot.exit(ticket);
    assertTrue(ticket.getSpot().isFree());
    assertTrue(fee >= 0);
}

๐Ÿ“Œ Summary

  • Vehicle + Spot hierarchies model type safety โ€” CompactSpot can only fit a CAR.
  • Min-Heap per vehicle type per floor gives O(log n) nearest-spot allocation vs O(n) scan.
  • Double-checked locking Singleton is thread-safe without synchronizing every park() call.
  • FeeCalculator (Strategy) decouples pricing rules from lot management logic.
  • Synchronized assign() / free() on ParkingSpot prevents double-booking under concurrent gate activity.

๐Ÿ“ Practice Quiz

  1. Why use a Min-Heap for available spots instead of a simple list with a linear scan?

    • A) Min-Heaps use less memory.
    • B) Min-Heap gives O(log n) find-nearest vs O(n) scan, and O(log n) re-insert on exit.
    • C) A list doesn't support multiple vehicle types.
      Answer: B
  2. What problem does double-checked locking solve in the ParkingLot Singleton?

    • A) It prevents the JVM from garbage-collecting the instance.
    • B) It ensures only one instance is created even when multiple threads enter getInstance() simultaneously at startup.
    • C) It prevents two threads from parking in the same spot.
      Answer: B
  3. Where in this design would you apply Dependency Inversion to make fee calculation swappable without changing ParkingLot?

    • A) Extract a FeeStrategy interface; inject it into ParkingLot's constructor. FeeCalculator becomes one implementation.
    • B) Move FeeCalculator inside ParkingSpot.
    • C) Store the fee rate table in the database.
      Answer: A

Abstract Algorithms

Written by

Abstract Algorithms

@abstractalgorithms