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 AlgorithmsTLDR: 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
Ticketissued 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.
๐ง Deep Dive: ParkingLot โ Singleton + Multi-Floor Search
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()issynchronizedat the floor level, allowing concurrent access across different floors.ParkingSpot.assign()issynchronizedat the spot level, preventing two threads from assigning the same spot.volatile ParkingLot instanceuses the Java Memory Model's happens-before guarantee: any thread that reads a non-nullinstanceis guaranteed to see the fully constructedParkingLotobject.
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
| Operation | Data Structure | Time Complexity | Notes |
| Find nearest spot | Min-Heap per floor per type | O(log n) peek + O(log n) poll | n = available spots of that type on floor |
| Release spot | Min-Heap re-insert | O(log n) | Spot ID re-inserted into heap |
| Park (single floor) | Heap poll | O(log n) | |
| Park (worst case, k floors) | k ร O(log n) | O(k log n) | Scans floors until spot found |
| Linear scan (alternative) | Array | O(n) find, O(1) release | Much 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.
| Pillar | Class / Interface | What It Buys Us |
| Encapsulation | ParkingFloor | Heap is private; findAndAssign() and releaseSpot() are the only doors in |
| Abstraction | FeeStrategy | exit() calls feeStrategy.calculate(ticket) โ pricing logic is invisible to the lot |
| Inheritance | BikeSpot, CompactSpot, LargeSpot | Each extends ParkingSpot, each overrides canFit(VehicleType) |
| Polymorphism | PriorityQueue<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
| Principle | Violated By | Respected By |
| SRP | ParkingFloor calculating fees | ParkingFloor (heap ops) + ParkingLot (fee delegation) staying separate |
| OCP | Switch-casing vehicle types | EvSpot extends ParkingSpot โ no change to ParkingFloor |
| LSP | A spot subtype with a broken getId() | All subtypes inherit a final-field-backed getId() |
| ISP | A bloated PricingService with 6 methods | @FunctionalInterface FeeStrategy with one method |
| DIP | ParkingLot hard-wiring FeeCalculator | ParkingLot(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 / Contract | Kind | Consumer | Purpose |
FeeStrategy | @FunctionalInterface | ParkingLot.exit() | Swappable pricing without modifying the lot |
| Heap comparator | Comparator<ParkingSpot> lambda | ParkingFloor | Nearest-spot ordering across all subtypes |
canFit(VehicleType) | Abstract method on ParkingSpot | ParkingFloor.findAndAssign() | Type-safe assignment guard |
โ๏ธ Trade-offs & Failure Modes: Design Patterns Applied
| Pattern | Where Used | Why |
| Singleton | ParkingLot.getInstance() | Single source of truth for lot state |
| Min-Heap | ParkingFloor.availableSpots | O(log n) nearest-spot allocation |
| Strategy | FeeCalculator | Swap pricing logic independently |
| Factory Method | VehicleFactory.create(type, plate) | Decouple creation from usage |
| Template Method | ParkingSpot.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.
| System | Analogous Pattern | Notes |
| Hospital room allocation | Min-Heap by room proximity to nursing station | Multi-floor search, typed rooms (ICU, general) |
| Airport gate assignment | Strategy for gate type (wide-body vs narrow-body) | Singleton gate manager, fee as turnaround time |
| Warehouse slot allocation | Min-Heap by pick path distance | Item type matching (cold storage, hazmat, standard) |
| Hotel room booking | Reservation system + availability heap | Type (king, queen, suite) with dynamic pricing |
| Cloud VM provisioning | Slot = VM type; floor = availability zone | Kubernetes 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.
| Decision | Option A | Option B | Choose When |
| Spot search | Min-Heap O(log n) | Linear scan O(n) | Use Min-Heap when > 50 spots per floor; scan is fine for toy systems |
| Concurrency | Synchronized at floor level | Lock-free ConcurrentHashMap | Synchronized simpler; lock-free for > 1000 concurrent gates |
| Fee calculation | Strategy interface | Static map in FeeCalculator | Use Strategy when pricing rules change independently of lot logic |
| Singleton | Double-checked locking | Enum singleton | Use Enum singleton in modern Java (simpler, thread-safe, serialization-safe) |
| Persistence | In-memory only | JPA entity + DB | Add 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.
synchronizedonfindAndAssign()combined withsynchronizedonassign()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
FeeCalculatorwith a rate map is extensible โ add a new vehicle type, add an entry to the map. Aswitch (vehicleType)block insideexit()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.
| Relationship | Type | Notes |
ParkingLot โ ParkingFloor | HAS-A (1 to many) | Lot owns its floors |
ParkingFloor โ ParkingSpot | HAS-A (manages many) | Floor owns its heap of spots |
ParkingLot โ FeeStrategy | USES (dependency) | Lot delegates fee calculation |
BikeSpot/CompactSpot/LargeSpot โ ParkingSpot | IS-A (extends) | Spot hierarchy |
Car/Truck/Bike โ Vehicle | IS-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
FeeStrategywithcalculate(Ticket)and inject it intoParkingLot's constructor."
Step 4 โ Identify creation complexity โ Factory Pattern.
"Creating
Vehiclesubtypes 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:
ParkingFloorhides itsPriorityQueueheap โ callers only seefindAndAssign()andreleaseSpot(). - Inheritance + canFit:
BikeSpot,CompactSpot,LargeSpotoverridecanFit(VehicleType)โ type safety withoutinstanceofchains. - Polymorphism:
PriorityQueue<ParkingSpot>orders all subtypes via one comparator;EvSpotslots in with zero heap changes. - Strategy (DIP):
ParkingLotacceptsFeeStrategyin 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
EvSpotextend cleanly; ISP keepsFeeStrategya one-method contract; DIP wires the lot to the abstraction, not the implementation.
๐ Practice Quiz: Test Your LLD Parking Lot Knowledge
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.
What problem does double-checked locking with
volatilesolve 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;volatileprevents reordering.
Where would you apply Dependency Inversion to make fee calculation swappable without modifying ParkingLot?
- A) Extract a
FeeStrategyinterface withcalculate(VehicleType, long), inject it intoParkingLot's constructor.FeeCalculatorbecomes one implementation ofFeeStrategy. - B) Move
FeeCalculatorlogic insideParkingSpot.free(). - C) Store the fee rate table in a database and query it at runtime.
- D) Make
FeeCalculatorextendParkingLotto 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.
- A) Extract a
ParkingFloor uses
synchronizedonfindAndAssign(). 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
ConcurrentLinkedQueueper 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 โConcurrentLinkedQueuewith compare-and-swap (CAS) operations avoids blocking. However, it loses the O(log n) nearest-spot ordering โ you'd need aConcurrentSkipListSetordered by spot ID for lock-free O(log n) operations.
(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
PeekAvailableSpotmethod; 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.
๐ Related Posts

Written by
Abstract Algorithms
@abstractalgorithms
More Posts

Types of LLM Quantization: By Timing, Scope, and Mapping
TLDR: There is no single "best" LLM quantization. You classify and choose quantization along three axes: when you quantize (timing), what you quantize (scope), and how values are encoded (mapping). In practice, most teams start with weight quantizati...
Stream Processing Pipeline Pattern: Stateful Real-Time Data Products
TLDR: Stream pipelines succeed when event-time semantics, state management, and replay strategy are designed together โ and Kafka Streams lets you build all three directly inside your Spring Boot service. Stripe's real-time fraud detection processes...
Service Mesh Pattern: Control Plane, Data Plane, and Zero-Trust Traffic
TLDR: A service mesh intercepts all service-to-service traffic via injected Envoy sidecar proxies, letting a platform team enforce mTLS, retries, timeouts, and circuit breaking centrally โ without changing application code. Reach for it when cross-te...
Serverless Architecture Pattern: Event-Driven Scale with Operational Guardrails
TLDR: Serverless is strongest for spiky asynchronous workloads when cold-start, observability, and state boundaries are intentionally designed. TLDR: Serverless works best for spiky, event-driven workloads when you design for idempotency, observabili...
