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
ยทยท25 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).


๐Ÿ“– TLDR: Design-to-Code โ€” What We Are Building

A parking lot LLD interview question trips up most candidates not because of the OOP โ€” but because interviewers expect you to spot the concurrency problem: two cars booking the same spot simultaneously. This post walks through the exact design that handles it.

Requirements we implement:

  • 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.

๐Ÿ”ข 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; }

    // Subclasses declare which vehicle types they accept
    public abstract boolean canFit(VehicleType type);
}

public class BikeSpot extends ParkingSpot {
    public BikeSpot(int id) { super(id, VehicleType.BIKE); }
    @Override public boolean canFit(VehicleType type) { return type == VehicleType.BIKE; }
}

public class CompactSpot extends ParkingSpot {
    public CompactSpot(int id) { super(id, VehicleType.CAR); }
    @Override public boolean canFit(VehicleType type) { return type == VehicleType.CAR; }
}

public class LargeSpot extends ParkingSpot {
    public LargeSpot(int id) { super(id, VehicleType.TRUCK); }
    // A large spot fits both trucks AND cars โ€” real-world nuance expressed via the override
    @Override public boolean canFit(VehicleType type) {
        return type == VehicleType.CAR || type == 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)"]

๐Ÿ“Š System Flow: Vehicle Entry to Exit

When a vehicle arrives at the gate, the request flows through three layers before a spot is assigned and a ticket issued. Understanding this top-down call chain makes it clear why synchronization lives at the floor level rather than the lot level.

flowchart TD
    A[Vehicle arrives at gate] --> B[Gate calls ParkingLot.park(vehicle)]
    B --> C[Iterate ParkingFloor list]
    C --> D{Floor has available\nspot for vehicle type?}
    D -->|No| C
    D -->|Yes| E[floor.findAndAssign(vehicle)\nMin-Heap poll O log n]
    E --> F[spot.assign(vehicle)\nsynchronized]
    F --> G[Issue Ticket\nvehicle + spot + floor + entryTime]
    G --> H[Vehicle parks]
    H --> I[Vehicle exits:\nlot.exit(ticket)]
    I --> J[floor.releaseSpot(spot)\nMin-Heap re-insert]
    J --> K[FeeCalculator.calculate\ntype + duration]
    K --> L[Return fee, spot freed]

The exit path mirrors entry in reverse: the spot is re-inserted into the floor's Min-Heap before the fee is calculated, so the spot becomes available to new arrivals immediately rather than after billing completes.


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 final FeeStrategy feeStrategy;

    private ParkingLot(FeeStrategy feeStrategy) {
        this.feeStrategy = feeStrategy;
    }

    public static ParkingLot getInstance(FeeStrategy feeStrategy) {
        if (instance == null) {
            synchronized (ParkingLot.class) {
                if (instance == null) {           // double-checked locking
                    instance = new ParkingLot(feeStrategy);
                }
            }
        }
        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) {
        ticket.getFloor().releaseSpot(ticket.getSpot());
        return feeStrategy.calculate(ticket);   // โ† delegates to injected strategy
    }
}

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

Internals: Thread Safety in ParkingLot

The park() method itself is not synchronized at the ParkingLot level โ€” synchronization happens at ParkingFloor.findAndAssign(). This is intentional: locking the entire lot for every park operation would serialize all gates. Instead:

  • ParkingFloor.findAndAssign() is synchronized at the floor level, allowing concurrent access across different floors.
  • ParkingSpot.assign() is synchronized at the spot level, preventing two threads from assigning the same spot.
  • volatile ParkingLot instance uses the Java Memory Model's happens-before guarantee: any thread that reads a non-null instance is guaranteed to see the fully constructed ParkingLot object.

This layered synchronization strategy โ€” lot-level for single instance, floor-level for search, spot-level for assignment โ€” minimizes contention while preventing double-booking.

Performance Analysis

OperationData StructureTime ComplexityNotes
Find nearest spotMin-Heap per floor per typeO(log n) peek + O(log n) polln = available spots of that type on floor
Release spotMin-Heap re-insertO(log n)Spot ID re-inserted into heap
Park (single floor)Heap pollO(log n)
Park (worst case, k floors)k ร— O(log n)O(k log n)Scans floors until spot found
Linear scan (alternative)ArrayO(n) find, O(1) releaseMuch slower for large floors

For a 5-floor lot with 100 spots per type per floor, Min-Heap gives O(log 100) โ‰ˆ 7 comparisons vs O(100) for linear scan. At 10,000 spots per floor, the gap is O(14) vs O(10,000).

Mathematical Model: Double-Checked Locking Correctness

The correctness of double-checked locking depends on the Java Memory Model (JMM):

T1 checks: instance == null โ†’ true โ†’ enters synchronized block
T1 checks: instance == null โ†’ true โ†’ creates instance โ†’ exits synchronized block
T2 checks: instance == null โ†’ false โ†’ returns existing instance (guaranteed by volatile)

Without volatile: The JVM can reorder instructions. instance might be assigned a reference before the constructor finishes executing. T2 would read a non-null but partially-constructed object โ€” a subtle race condition. The volatile keyword introduces a memory barrier: all writes before the volatile write are visible to any thread that subsequently reads the volatile variable.

Proof: Java Language Specification ยง17.4.5 defines the happens-before relationship. volatile write โ†’ happens-before โ†’ volatile read. Therefore, T2's read of instance != null establishes that all of T1's constructor writes have completed.


โš™๏ธ 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.


๐Ÿงฑ OOP Pillars in the Implementation

Every OOP pillar is load-bearing here โ€” not decorative. The table below maps each pillar to the exact class or interface where it does work, followed by the code rationale.

PillarClass / InterfaceWhat It Buys Us
EncapsulationParkingFloorHeap is private; findAndAssign() and releaseSpot() are the only doors in
AbstractionFeeStrategyexit() calls feeStrategy.calculate(ticket) โ€” pricing logic is invisible to the lot
InheritanceBikeSpot, CompactSpot, LargeSpotEach extends ParkingSpot, each overrides canFit(VehicleType)
PolymorphismPriorityQueue<ParkingSpot>Heap holds all spot subtypes; the comparator works regardless of concrete type

Encapsulation: ParkingFloor Owns Its Heap

The Map<VehicleType, PriorityQueue<ParkingSpot>> inside ParkingFloor is private. External callers cannot poll the heap, reorder it, or add a spot to the wrong vehicle-type bucket. They call two synchronized methods and the floor handles the rest:

floor.findAndAssign(vehicle);   // caller never touches the heap directly
floor.releaseSpot(spot);        // re-insertion is the floor's responsibility

This boundary is what makes the floor's Min-Heap an implementation detail rather than a public contract. If you later swap the heap for a ConcurrentSkipListSet, no caller changes.

Abstraction: FeeStrategy Shields ParkingLot from Pricing Logic

ParkingLot.exit() calls feeStrategy.calculate(ticket). Whether the strategy applies hourly rates, flat fees, or peak surcharges is completely invisible to the lot. Replacing pricing is a one-line constructor change โ€” no if/switch chains buried in exit():

// Hourly pricing โ€” injected at startup
FeeStrategy hourly = ticket -> {
    long hours = Math.max(1,
        Duration.between(ticket.getEntryTime(), Instant.now()).toHours());
    return hours * BASE_RATES.get(ticket.getVehicle().getType());
};

ParkingLot lot = ParkingLot.getInstance(hourly);

Inheritance: canFit Enforces Type Compatibility

The abstract canFit(VehicleType) method forces every subclass to declare which vehicles it accepts. The LargeSpot override expresses a real-world nuance that flat type equality cannot:

// LargeSpot can accommodate both trucks and cars โ€” not expressible without override
public class LargeSpot extends ParkingSpot {
    public LargeSpot(int id) { super(id, VehicleType.TRUCK); }
    @Override
    public boolean canFit(VehicleType type) {
        return type == VehicleType.CAR || type == VehicleType.TRUCK;
    }
}

ParkingFloor.findAndAssign() can call spot.canFit(v.getType()) as a guard before assignment โ€” the check is polymorphic and requires no instanceof.

Polymorphism: The Heap Is Subtype-Agnostic

The PriorityQueue<ParkingSpot> comparator calls only getId() โ€” a method defined in the abstract base class with a final-field backing it:

new PriorityQueue<>((a, b) -> a.getId() - b.getId())

BikeSpot #3 and LargeSpot #7 are ordered identically. The heap is entirely unaware of concrete types. Add EvSpot, add MotorcycleSpot โ€” the heap ordering logic never changes.


โœ… SOLID Principles Applied in Code

PrincipleViolated ByRespected By
SRPParkingFloor calculating feesParkingFloor (heap ops) + ParkingLot (fee delegation) staying separate
OCPSwitch-casing vehicle typesEvSpot extends ParkingSpot โ€” no change to ParkingFloor
LSPA spot subtype with a broken getId()All subtypes inherit a final-field-backed getId()
ISPA bloated PricingService with 6 methods@FunctionalInterface FeeStrategy with one method
DIPParkingLot hard-wiring FeeCalculatorParkingLot(FeeStrategy) โ€” lot depends on the abstraction

SRP โ€” The floor knows spots; the lot knows fees. If ParkingFloor.findAndAssign() also invoked feeStrategy.calculate(), then changing pricing rules would require opening ParkingFloor. Two reasons to change = SRP violation. The floor's single responsibility: maintain an ordered set of available spots per vehicle type.

OCP โ€” New spot type, zero changes to ParkingFloor. Adding EV charging to the lot means one new class:

public class EvSpot extends ParkingSpot {
    private final boolean hasCharger;

    public EvSpot(int id, boolean hasCharger) {
        super(id, VehicleType.CAR);           // reuses a CAR-type heap slot
        this.hasCharger = hasCharger;
    }

    @Override
    public boolean canFit(VehicleType type) { return type == VehicleType.CAR; }
    public boolean hasCharger() { return hasCharger; }
}

ParkingFloor accepts EvSpot in its CAR heap immediately โ€” the comparator only calls getId(), which EvSpot inherits unchanged.

LSP โ€” Every subtype is substitutable in the PriorityQueue. The heap invariant requires the comparator to be consistent and total. Every ParkingSpot subtype inherits getId() without override, so no subtype can return an inconsistent value. An EvSpot in a BikeSpot heap would be an LSP violation at the caller level (wrong type bucket) โ€” which is why ParkingFloor partitions its heaps by VehicleType.

ISP โ€” FeeStrategy carries exactly one method. The @FunctionalInterface annotation enforces this at compile time. A peak-hour implementation doesn't inherit an unused calculateMembership() method from a fatter interface:

@FunctionalInterface
public interface FeeStrategy {
    double calculate(Ticket ticket);   // one method, no baggage
}

DIP โ€” ParkingLot depends on the abstraction, not FeeCalculator. The updated ParkingLot constructor receives FeeStrategy โ€” a high-level abstraction. The concrete FeeCalculator is one possible implementation, but ParkingLot never imports it:

// At application bootstrap โ€” the only place that knows the concrete type
FeeStrategy hourlyStrategy = ticket -> { /* ... */ };
ParkingLot lot = ParkingLot.getInstance(hourlyStrategy);

The Singleton guarantees a single instance; DI guarantees the instance is not coupled to any specific pricing class.


๐Ÿ“ Interface Contracts: FeeStrategy and the Heap Comparator

Two interface contracts hold the design together. Neither is a full class hierarchy โ€” both are minimal, focused obligations.

FeeStrategy: The @FunctionalInterface Contract

@FunctionalInterface
public interface FeeStrategy {
    double calculate(Ticket ticket);
}

// Hourly rate โ€” lambda, no extra class file
FeeStrategy hourly = ticket -> {
    long hours = Math.max(1,
        Duration.between(ticket.getEntryTime(), Instant.now()).toHours());
    return hours * BASE_RATES.get(ticket.getVehicle().getType());
};

// Flat rate โ€” ignores duration entirely
FeeStrategy flat = ticket -> 50.0;

// Peak-hour surcharge โ€” strategy reads exit context from ticket
FeeStrategy peakHour = ticket -> {
    long hours = Math.max(1,
        Duration.between(ticket.getEntryTime(), Instant.now()).toHours());
    boolean isPeak = LocalTime.now().isAfter(LocalTime.of(17, 0))
                  && LocalTime.now().isBefore(LocalTime.of(19, 0));
    double base = BASE_RATES.get(ticket.getVehicle().getType());
    return base * hours * (isPeak ? 2.0 : 1.0);
};

@FunctionalInterface is the OOP tool that turns the Strategy Pattern from a three-class hierarchy (AbstractFeeStrategy โ†’ HourlyFeeStrategy, FlatFeeStrategy) into a one-liner lambda. The annotation also serves as self-documenting API contract: anyone reading the interface immediately knows it is safely lambda-assignable.

Passing Ticket as the single parameter โ€” rather than (VehicleType, long) โ€” is intentional: Ticket is the value object that encapsulates all context (vehicle, spot, floor, entry time). Adding a new contextual field to Ticket never changes the FeeStrategy method signature.

The Heap Comparator: Implicit Contract for PriorityQueue\

// Defined once in ParkingFloor constructor; all subtypes comply automatically
Comparator<ParkingSpot> byId = (a, b) -> a.getId() - b.getId();
new PriorityQueue<>(byId)

Every ParkingSpot subtype participates in the heap ordering via this comparator. Because getId() is backed by a final int id field set in the abstract constructor, no subtype can return an inconsistent value.

Interface / ContractKindConsumerPurpose
FeeStrategy@FunctionalInterfaceParkingLot.exit()Swappable pricing without modifying the lot
Heap comparatorComparator<ParkingSpot> lambdaParkingFloorNearest-spot ordering across all subtypes
canFit(VehicleType)Abstract method on ParkingSpotParkingFloor.findAndAssign()Type-safe assignment guard

โš–๏ธ Trade-offs & Failure Modes: 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

๐Ÿ—๏ธ Advanced Extension Points: Dynamic Pricing, Reservations, and EV Charging

The core design handles the standard case cleanly, but real parking systems need more. Here are three natural extension points โ€” each slots in without modifying existing classes.

Dynamic Pricing with Strategy Pattern: Currently FeeCalculator uses flat rates. To support peak pricing (2ร— rates from 5โ€“7 pm), extract a FeeStrategy interface:

public interface FeeStrategy {
    double calculate(VehicleType type, long minutes, LocalTime exitTime);
}

public class PeakHourFeeStrategy implements FeeStrategy {
    public double calculate(VehicleType type, long minutes, LocalTime exitTime) {
        double hours = Math.ceil(minutes / 60.0);
        double baseRate = BASE_RATES.get(type);
        boolean isPeak = exitTime.isAfter(LocalTime.of(17, 0))
                      && exitTime.isBefore(LocalTime.of(19, 0));
        return baseRate * hours * (isPeak ? 2.0 : 1.0);
    }
}

Inject FeeStrategy into ParkingLot's constructor. Now pricing rules are swappable without touching lot management logic โ€” a clean application of the Open/Closed Principle.

Reservation System: Add a ReservationService that pre-allocates spots for timed windows. The Min-Heap needs to be partitioned: a walk-in heap vs a reserved heap per vehicle type. On reservation, move the spot from the walk-in heap to the reserved heap. On reservation expiry, move it back.

EV Charging Spots: Add EVSpot extends CompactSpot with a chargerType field (Level 2, DC Fast Charge). The heap for the CAR type would separate EVSpot and regular CompactSpot using a custom comparator that prioritizes charger proximity when the requesting vehicle is flagged as electric. No changes to ParkingLot or ParkingFloor are needed โ€” only the comparator and spot subclass.


๐Ÿงช 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);
}

๐ŸŒ Real-World Applications: Where This Design Applies

The core pattern โ€” typed resource allocation, nearest-first selection, concurrent access control, and pluggable pricing โ€” recurs across many industries. Recognizing it in a new problem lets you reuse the same structural decisions rather than redesigning from scratch.

SystemAnalogous PatternNotes
Hospital room allocationMin-Heap by room proximity to nursing stationMulti-floor search, typed rooms (ICU, general)
Airport gate assignmentStrategy for gate type (wide-body vs narrow-body)Singleton gate manager, fee as turnaround time
Warehouse slot allocationMin-Heap by pick path distanceItem type matching (cold storage, hazmat, standard)
Hotel room bookingReservation system + availability heapType (king, queen, suite) with dynamic pricing
Cloud VM provisioningSlot = VM type; floor = availability zoneKubernetes scheduler uses similar bin-packing logic

Any system requiring typed resource allocation with nearest-first assignment and concurrent access benefits from this structure. The Singleton manages global state and ensures a single source of truth; the Min-Heap provides efficient nearest-first selection without scanning every resource; the Strategy pattern decouples pricing or allocation policy logic so rules can change without touching the core allocation engine.


๐Ÿงญ Decision Guide: When to Use Which Pattern

Not every problem needs every pattern. The table below captures the tradeoffs so you can make an informed choice rather than applying patterns by default.

DecisionOption AOption BChoose When
Spot searchMin-Heap O(log n)Linear scan O(n)Use Min-Heap when > 50 spots per floor; scan is fine for toy systems
ConcurrencySynchronized at floor levelLock-free ConcurrentHashMapSynchronized simpler; lock-free for > 1000 concurrent gates
Fee calculationStrategy interfaceStatic map in FeeCalculatorUse Strategy when pricing rules change independently of lot logic
SingletonDouble-checked lockingEnum singletonUse Enum singleton in modern Java (simpler, thread-safe, serialization-safe)
PersistenceIn-memory onlyJPA entity + DBAdd persistence layer when lot state must survive server restart

The core principle: patterns solve specific forces โ€” concurrency, extensibility, single truth of state. Don't apply them universally; apply them where the force is present. A Min-Heap is wasted complexity on a 10-spot test lot. A Singleton is essential when the lot state must be globally consistent across gate threads.


๐Ÿ“š Lessons from LLD Interviews and Production Code

These lessons come from patterns seen repeatedly in both interview whiteboard sessions and production Java codebases.

  • Lesson 1: Start with the domain model, not the patterns. In interviews, candidates who draw the class diagram first (Vehicle, Spot, Ticket, Floor, Lot) before mentioning Singleton consistently score higher than those who open with "I'll use a Singleton." The domain model reveals what needs a pattern; the pattern doesn't define the domain.

  • Lesson 2: Thread safety is a first-class requirement. The question "what happens if two cars try to park simultaneously?" is almost always asked. synchronized on findAndAssign() combined with synchronized on assign() is the simplest correct answer. Lock-free structures are an optimization discussion โ€” mention them only after the correct baseline is established.

  • Lesson 3: Min-Heap is the key insight. Interviewers reward candidates who identify that "find nearest available spot" is a priority queue problem, not a linear search problem. State the complexity explicitly: O(log n) vs O(n). The difference at 1000 spots is ~10 comparisons vs up to 1000. That single insight often separates good from great answers.

  • Lesson 4: Strategy over switch statements. A FeeCalculator with a rate map is extensible โ€” add a new vehicle type, add an entry to the map. A switch (vehicleType) block inside exit() is a closed design: every new vehicle type requires modifying the method. That violates the Open/Closed Principle and is a common interview red flag.


๐ŸŽฏ OOP Interview Walk-Through: Presenting This Design on a Whiteboard

Interviewers don't just evaluate the design โ€” they evaluate the thought process. Here is the step-by-step narration that earns maximum signal.

Step 1 โ€” State the problem, identify the nouns.

"A parking lot has multiple floors. Vehicles of three types enter via gates. Each type maps to a compatible spot type. On entry, a ticket is issued. On exit, a fee is calculated."

Nouns โ†’ classes: Vehicle, ParkingSpot, Ticket, ParkingFloor, ParkingLot.

Step 2 โ€” Identify relationships and draw the class diagram.

RelationshipTypeNotes
ParkingLot โ†’ ParkingFloorHAS-A (1 to many)Lot owns its floors
ParkingFloor โ†’ ParkingSpotHAS-A (manages many)Floor owns its heap of spots
ParkingLot โ†’ FeeStrategyUSES (dependency)Lot delegates fee calculation
BikeSpot/CompactSpot/LargeSpot โ†’ ParkingSpotIS-A (extends)Spot hierarchy
Car/Truck/Bike โ†’ VehicleIS-A (extends)Vehicle hierarchy
classDiagram
    class ParkingLot {
        <>
        -List~ParkingFloor~ floors
        -FeeStrategy feeStrategy
        +park(Vehicle) Ticket
        +exit(Ticket) double
    }
    class ParkingFloor {
        -Map~VehicleType, PriorityQueue~ availableSpots
        +findAndAssign(Vehicle) ParkingSpot
        +releaseSpot(ParkingSpot) void
    }
    class ParkingSpot {
        <>
        +canFit(VehicleType) boolean
        +assign(Vehicle) boolean
        +free() void
    }
    class FeeStrategy {
        <>
        +calculate(Ticket) double
    }
    class Vehicle {
        <>
        +getType() VehicleType
    }
    ParkingLot "1" --> "*" ParkingFloor : has-a
    ParkingFloor "1" --> "*" ParkingSpot : manages
    ParkingLot --> FeeStrategy : uses
    ParkingSpot <|-- BikeSpot
    ParkingSpot <|-- CompactSpot
    ParkingSpot <|-- LargeSpot
    Vehicle <|-- Car
    Vehicle <|-- Truck
    Vehicle <|-- Bike

Step 3 โ€” Identify the varying part โ†’ Strategy Pattern.

"Pricing changes independently of parking logic. I'll extract FeeStrategy with calculate(Ticket) and inject it into ParkingLot's constructor."

Step 4 โ€” Identify creation complexity โ†’ Factory Pattern.

"Creating Vehicle subtypes from a string-typed gate request is a Factory. VehicleFactory.create(plate, type) keeps construction out of the controller."

Step 5 โ€” Identify shared global state โ†’ Singleton Pattern.

"The lot is a single source of truth across all entry gate threads. Singleton with double-checked locking and volatile โ€” one instance, thread-safe initialization, no synchronization cost after startup."

Step 6 โ€” Address concurrency proactively โ€” before the interviewer asks.

"Two gates can try to assign the same spot simultaneously. I synchronize at ParkingFloor.findAndAssign() โ€” floor-level granularity, not lot-level. Different floors can serve different gates concurrently. ParkingSpot.assign() is also synchronized as a safety net against cross-floor races."

This is the sequence interviewers most commonly follow up on. Raising it first signals production-readiness thinking.


๐Ÿ“Œ TLDR: Summary & Key Takeaways

  • Encapsulation: ParkingFloor hides its PriorityQueue heap โ€” callers only see findAndAssign() and releaseSpot().
  • Inheritance + canFit: BikeSpot, CompactSpot, LargeSpot override canFit(VehicleType) โ€” type safety without instanceof chains.
  • Polymorphism: PriorityQueue<ParkingSpot> orders all subtypes via one comparator; EvSpot slots in with zero heap changes.
  • Strategy (DIP): ParkingLot accepts FeeStrategy in its constructor โ€” pricing is swappable without touching lot logic.
  • Min-Heap per type per floor: O(log n) nearest-spot allocation vs O(n) scan โ€” the single insight that separates good from great interview answers.
  • Double-checked locking Singleton: volatile + two null checks = thread-safe initialization with no per-call sync overhead.
  • SOLID checkpoint: SRP separates floor/lot concerns; OCP lets EvSpot extend cleanly; ISP keeps FeeStrategy a one-method contract; DIP wires the lot to the abstraction, not the implementation.

๐Ÿ“ Practice Quiz: Test Your LLD Parking Lot Knowledge

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

    • A) Min-Heaps use less memory than Lists.
    • B) Min-Heap gives O(log n) find-nearest and O(log n) re-insert on exit, versus O(n) linear scan โ€” critical when floors have hundreds of spots.
    • C) Lists don't support concurrent access; Min-Heaps do natively.
    • D) Min-Heaps automatically sort spots by vehicle type. Correct Answer: B โ€” The heap is ordered by spot ID (smallest = nearest to entrance). poll() gives the nearest available spot in O(log n). Linear scan requires checking all spots: O(n). At 1000 spots, heap takes ~10 comparisons; linear takes up to 1000.
  2. What problem does double-checked locking with volatile solve in the ParkingLot Singleton?

    • A) It prevents the JVM from garbage-collecting the ParkingLot instance.
    • B) It ensures only one ParkingLot instance is created even when multiple threads call getInstance() simultaneously at startup, while avoiding synchronization overhead on every subsequent call.
    • C) It prevents two threads from parking in the same spot.
    • D) It ensures the Singleton survives application restart. Correct Answer: B โ€” Without volatile, the JVM can reorder instructions and expose a partially-constructed object to other threads. The outer null check avoids synchronization after initialization; the inner check prevents duplicate creation; volatile prevents reordering.
  3. Where would you apply Dependency Inversion to make fee calculation swappable without modifying ParkingLot?

    • A) Extract a FeeStrategy interface with calculate(VehicleType, long), inject it into ParkingLot's constructor. FeeCalculator becomes one implementation of FeeStrategy.
    • B) Move FeeCalculator logic inside ParkingSpot.free().
    • C) Store the fee rate table in a database and query it at runtime.
    • D) Make FeeCalculator extend ParkingLot to access vehicle type directly. Correct Answer: A โ€” Dependency Inversion: high-level modules (ParkingLot) should not depend on low-level modules (FeeCalculator); both should depend on abstractions (FeeStrategy). This allows swapping pricing logic (flat rate, peak pricing, subscription) without touching ParkingLot.
  4. ParkingFloor uses synchronized on findAndAssign(). Under extremely high concurrency (1000 simultaneous arrivals), this becomes a bottleneck. What would you change and why?

    • A) Remove synchronization entirely โ€” race conditions are rare in practice.
    • B) Use a ConcurrentLinkedQueue per vehicle type instead of synchronized PriorityQueue โ€” lock-free operations reduce contention.
    • C) Add a ReentrantReadWriteLock โ€” readers never conflict with each other.
    • D) Use a CopyOnWriteArrayList โ€” writes are infrequent in practice. Correct Answer: B โ€” ConcurrentLinkedQueue with compare-and-swap (CAS) operations avoids blocking. However, it loses the O(log n) nearest-spot ordering โ€” you'd need a ConcurrentSkipListSet ordered by spot ID for lock-free O(log n) operations.
  5. (Open-ended) The current design assigns spots floor-by-floor starting from floor 1. A premium customer arrives and requests the spot closest to the elevator on any floor, not just the nearest on floor 1. Describe the changes to ParkingFloor and ParkingLot needed to support this, and what new data structure or comparison logic you would add. Correct Answer: This is an open-ended design challenge โ€” there is no single correct answer. One approach: add a global priority queue across all floors, where each entry is (floorNumber, spotId) with a custom comparator that weights elevator proximity. ParkingLot.park() would poll this global heap instead of iterating floors. The tradeoff: a global heap requires global synchronization (contention) vs per-floor heaps (concurrent floors). An alternative: maintain per-floor heaps but expose a PeekAvailableSpot method; ParkingLot iterates all floors' peeks and selects the best one โ€” O(k log n) with k=floors.


๐Ÿ› ๏ธ Spring Boot in Action: Wrapping the Parking Lot as a Production REST Service

Spring Boot is an opinionated Java framework that auto-configures an embedded server, dependency injection, and web layer so you can stand up a REST API in minutes. Annotating the ParkingLotService with @Service wires it into the IoC container, and @RestController exposes it over HTTP with zero XML config.

How it solves the problem in this post: The ParkingLot Singleton and ParkingFloor allocation logic we built become a Spring-managed service bean. Spring's singleton scope guarantees one ParkingLotService instance JVM-wide โ€” reinforcing the Singleton pattern. @Transactional (with optimistic locking) can replace the raw synchronized blocks for database-backed persistence later.

// --- Service layer: wraps ParkingLot singleton inside Spring IoC ---
@Service
public class ParkingLotService {

    private final ParkingLot lot = ParkingLot.getInstance(); // Singleton

    /** Entry gate: park vehicle, return ticket JSON */
    public Ticket park(String plate, VehicleType type) {
        Vehicle vehicle = VehicleFactory.create(plate, type);
        return lot.park(vehicle)
                  .orElseThrow(() -> new ResponseStatusException(
                          HttpStatus.CONFLICT, "No available spot for " + type));
    }

    /** Exit gate: free spot, return fee */
    public double exit(String ticketId) {
        return lot.exit(ticketId);
    }

    public ParkingLotStatus status() {
        return lot.getStatus(); // available spots per floor
    }
}

// --- REST controller: exposes HTTP endpoints ---
@RestController
@RequestMapping("/api/parking")
public class ParkingLotController {

    private final ParkingLotService service;

    public ParkingLotController(ParkingLotService service) {
        this.service = service;
    }

    /** POST /api/parking/enter?plate=ABC123&type=CAR */
    @PostMapping("/enter")
    public ResponseEntity<Ticket> enter(
            @RequestParam String plate,
            @RequestParam VehicleType type) {
        Ticket ticket = service.park(plate, type);
        return ResponseEntity.status(HttpStatus.CREATED).body(ticket);
    }

    /** POST /api/parking/exit/{ticketId} โ†’ returns fee */
    @PostMapping("/exit/{ticketId}")
    public ResponseEntity<Map<String, Object>> exit(
            @PathVariable String ticketId) {
        double fee = service.exit(ticketId);
        return ResponseEntity.ok(Map.of("ticketId", ticketId, "fee", fee));
    }

    /** GET /api/parking/status โ†’ available spots summary */
    @GetMapping("/status")
    public ResponseEntity<ParkingLotStatus> status() {
        return ResponseEntity.ok(service.status());
    }
}

Run with mvn spring-boot:run. The embedded Tomcat starts on port 8080. POST /api/parking/enter?plate=KA01AB1234&type=CAR returns a Ticket JSON body; POST /api/parking/exit/{id} returns the calculated fee. The synchronized methods in ParkingSpot.assign() ensure concurrent HTTP requests from multiple entry gates don't double-book a spot.

For a full deep-dive on Spring Boot REST API patterns and concurrent parking lot state management, a dedicated follow-up post is planned.



Abstract Algorithms

Written by

Abstract Algorithms

@abstractalgorithms