🚗 Parking Lot Design | System Design + LLD + Full Code Implementation
This chapter explores the end-to-end Low-Level Design (LLD) of a robust, extensible, and maintainable Parking Lot System. We will walk through the entire lifecycle of an interview—from clarifying requirements and mapping core design patterns to writing clean, production-ready code and extending the system to support complex multi-floor configurations.
1. Why Do We Need This? (The Problem Landscape)
A parking lot system is a classic object-oriented design problem that tests your ability to model real-world physical constraints into software abstractions. As a system designer, you must shift your perspective from a casual customer to a parking lot manager.
The Core Challenges
- Space Management: Efficiently allocating and tracking limited physical spots for highly diverse vehicle types (e.g., bikes, cars, trucks).
- Dynamic Pricing: Calculating fees based on variable factors such as duration, vehicle type, and peak hours or special events.
- Payment Flexibility: Supporting multiple payment channels (cash, credit card, UPI) seamlessly.
- Consistency & Synchronization: Preventing illegal states, such as double-booking a single spot or allowing a vehicle to exit without completing payment.
2. Core Mental Model & Real-World Analogies
To design an elegant system, we can draw inspiration from real-world physical parking architectures:
The Airport Parking Analogy (Dynamic Pricing)
If you have ever parked at an airport, you know that pricing is rarely flat. It is highly dynamic, offering hourly rates, daily packages, premium valet options, or holiday surcharges. Our system must decouple the pricing logic from the vehicle entity so that pricing rules can change at runtime without modifying core vehicle classes.
The Mall Floor Distribution Analogy (Floor Configurations)
In modern multi-floor mall garages, space is optimized strategically:
- Ground/Lower Levels: Reserved for high-turnover or specialized vehicles (e.g., taxis, rideshares, disabled parking, or motorcycles).
- Upper Levels: Allocated for standard passenger cars.
- Roof/Basement Levels: Allocated for oversized vehicles or long-term parking.
Our design must support custom spot distributions per floor dynamically, rather than hardcoding a uniform layout.
3. Requirements Gathering & Interview Strategy
In an interview, you must systematically define the scope. Avoid jumping straight into coding. Instead, use a structured, pointwise enaction of the customer journey:
Clarifying Questions to Ask the Interviewer
- "Should we support multiple vehicle types and corresponding spot sizes?"
- Expected Answer: Yes (e.g., Bike, Car, Truck).
- "Do we need to support dynamic or variable pricing strategies?"
- Expected Answer: Yes, pricing can vary by vehicle type and duration.
- "Is multi-floor parking a requirement from day one?"
- Expected Answer: Start with a single floor, but ensure the architecture is highly extensible to support multiple floors later.
- "How should we handle concurrent bookings?"
- Expected Answer: For this LLD session, focus on the structural classes first, then discuss thread-safety and synchronization.
4. Architectural Design Patterns Overview
To build a clean, decoupled system, we will leverage several classic Gang of Four (GoF) design patterns:
5. Core Entities & Bottom-Up Class Design
To avoid getting overwhelmed, we design our system using a bottom-up approach. We start by defining the smallest, independent components (Strategies, Vehicles) and gradually compose them into larger orchestrators (Spots, Floors, Parking Lot).
6. Step-by-Step Evolution: Strategy Pattern for Pricing & Payments
We begin by defining our strategies. This ensures that pricing and payment behaviors are completely decoupled from the physical entities.
A. Parking Fee Strategy
This strategy calculates the total fee based on the duration of stay.
// ParkingFeeStrategy.java
public interface ParkingFeeStrategy {
double calculateFee(long durationInMinutes);
}
// BasicHourlyStrategy.java
public class BasicHourlyStrategy implements ParkingFeeStrategy {
private static final double HOURLY_RATE = 20.0;
@Override
public double calculateFee(long durationInMinutes) {
double hours = Math.ceil(durationInMinutes / 60.0);
return hours * HOURLY_RATE;
}
}
// PremiumStrategy.java
public class PremiumStrategy implements ParkingFeeStrategy {
private static final double PREMIUM_HOURLY_RATE = 50.0;
@Override
public double calculateFee(long durationInMinutes) {
double hours = Math.ceil(durationInMinutes / 60.0);
return hours * PREMIUM_HOURLY_RATE;
}
}
B. Payment Strategy
This strategy handles the actual transaction processing.
// PaymentStrategy.java
public interface PaymentStrategy {
boolean processPayment(double amount);
}
// CashPayment.java
public class CashPayment implements PaymentStrategy {
@Override
public boolean processPayment(double amount) {
System.out.println("Processing cash payment of $" + amount);
return true; // Assume payment is always successful for simplicity
}
}
// CreditCardPayment.java
public class CreditCardPayment implements PaymentStrategy {
private String cardNumber;
public CreditCardPayment(String cardNumber) {
this.cardNumber = cardNumber;
}
@Override
public boolean processPayment(double amount) {
System.out.println("Processing credit card payment of $" + amount + " using card: " + cardNumber);
return true;
}
}
7. Step-by-Step Evolution: Vehicle Hierarchy & Factory Pattern
Next, we define the Vehicle abstraction. Each vehicle is assigned a specific ParkingFeeStrategy upon creation.
// VehicleType.java
public enum VehicleType {
CAR, BIKE, TRUCK
}
// Vehicle.java
public abstract class Vehicle {
private final String licensePlate;
private final VehicleType vehicleType;
private final ParkingFeeStrategy feeStrategy;
public Vehicle(String licensePlate, VehicleType vehicleType, ParkingFeeStrategy feeStrategy) {
this.licensePlate = licensePlate;
this.vehicleType = vehicleType;
this.feeStrategy = feeStrategy;
}
public String getLicensePlate() { return licensePlate; }
public VehicleType getVehicleType() { return vehicleType; }
public ParkingFeeStrategy getFeeStrategy() { return feeStrategy; }
}
// Car.java
public class Car extends Vehicle {
public Car(String licensePlate, ParkingFeeStrategy feeStrategy) {
super(licensePlate, VehicleType.CAR, feeStrategy);
}
}
// Bike.java
public class Bike extends Vehicle {
public Bike(String licensePlate, ParkingFeeStrategy feeStrategy) {
super(licensePlate, VehicleType.BIKE, feeStrategy);
}
}
Vehicle Factory
The factory simplifies object creation, ensuring that the correct pricing strategy is bound to the vehicle type.
// VehicleFactory.java
public class VehicleFactory {
public static Vehicle createVehicle(VehicleType type, String licensePlate) {
switch (type) {
case CAR:
return new Car(licensePlate, new BasicHourlyStrategy());
case BIKE:
return new Bike(licensePlate, new BasicHourlyStrategy());
default:
throw new IllegalArgumentException("Unsupported vehicle type: " + type);
}
}
}
8. Step-by-Step Evolution: Parking Spots & Allocation Logic
A parking spot represents a physical location in the lot. It has a specific size (type) and can hold a vehicle of a compatible type.
// ParkingSpot.java
public abstract class ParkingSpot {
private final String spotNumber;
private final VehicleType supportedType;
private Vehicle parkedVehicle;
private boolean isOccupied;
public ParkingSpot(String spotNumber, VehicleType supportedType) {
this.spotNumber = spotNumber;
this.supportedType = supportedType;
this.isOccupied = false;
}
public synchronized boolean parkVehicle(Vehicle vehicle) {
if (isOccupied) {
return false;
}
if (!canParkVehicle(vehicle)) {
throw new IllegalArgumentException("Vehicle type " + vehicle.getVehicleType() + " cannot park in a " + supportedType + " spot.");
}
this.parkedVehicle = vehicle;
this.isOccupied = true;
return true;
}
public synchronized void vacate() {
this.parkedVehicle = null;
this.isOccupied = false;
}
public abstract boolean canParkVehicle(Vehicle vehicle);
// Getters
public String getSpotNumber() { return spotNumber; }
public VehicleType getSupportedType() { return supportedType; }
public Vehicle getParkedVehicle() { return parkedVehicle; }
public boolean isOccupied() { return isOccupied; }
}
// CarParkingSpot.java
public class CarParkingSpot extends ParkingSpot {
public CarParkingSpot(String spotNumber) {
super(spotNumber, VehicleType.CAR);
}
@Override
public boolean canParkVehicle(Vehicle vehicle) {
return vehicle.getVehicleType() == VehicleType.CAR;
}
}
// BikeParkingSpot.java
public class BikeParkingSpot extends ParkingSpot {
public BikeParkingSpot(String spotNumber) {
super(spotNumber, VehicleType.BIKE);
}
@Override
public boolean canParkVehicle(Vehicle vehicle) {
return vehicle.getVehicleType() == VehicleType.BIKE;
}
}
9. Step-by-Step Evolution: The Parking Lot & Main Execution Flow
Now, we build a single-floor ParkingLot manager to tie these components together.
// Ticket.java
import java.time.Instant;
public class Ticket {
private final String ticketId;
private final Vehicle vehicle;
private final ParkingSpot spot;
private final Instant startTime;
public Ticket(String ticketId, Vehicle vehicle, ParkingSpot spot) {
this.ticketId = ticketId;
this.vehicle = vehicle;
this.spot = spot;
this.startTime = Instant.now();
}
public String getTicketId() { return ticketId; }
public Vehicle getVehicle() { return vehicle; }
public ParkingSpot getSpot() { return spot; }
public Instant getStartTime() { return startTime; }
}
// ParkingLot.java
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
public class ParkingLot {
private final List<ParkingSpot> spots;
public ParkingLot() {
this.spots = new ArrayList<>();
}
public void addParkingSpot(ParkingSpot spot) {
spots.add(spot);
}
public synchronized Ticket entry(Vehicle vehicle) {
ParkingSpot availableSpot = findAvailableSpot(vehicle.getVehicleType());
if (availableSpot != null) {
availableSpot.parkVehicle(vehicle);
String ticketId = UUID.randomUUID().toString();
Ticket ticket = new Ticket(ticketId, vehicle, availableSpot);
System.out.println("Vehicle " + vehicle.getLicensePlate() + " parked at spot " + availableSpot.getSpotNumber());
return ticket;
}
System.out.println("No available spots for vehicle type: " + vehicle.getVehicleType());
return null;
}
public synchronized double exit(Ticket ticket, PaymentStrategy paymentStrategy, long simulatedDurationMinutes) {
ParkingSpot spot = ticket.getSpot();
Vehicle vehicle = ticket.getVehicle();
// Calculate Fee
double fee = vehicle.getFeeStrategy().calculateFee(simulatedDurationMinutes);
System.out.println("Calculated fee for " + simulatedDurationMinutes + " mins: $" + fee);
// Process Payment
boolean paymentSuccess = paymentStrategy.processPayment(fee);
if (paymentSuccess) {
spot.vacate();
System.out.println("Payment successful. Spot " + spot.getSpotNumber() + " is now vacant.");
return fee;
} else {
System.out.println("Payment failed. Vehicle cannot exit.");
return -1;
}
}
private ParkingSpot findAvailableSpot(VehicleType type) {
for (ParkingSpot spot : spots) {
if (!spot.isOccupied() && spot.getSupportedType() == type) {
return spot;
}
}
return null;
}
}
Single-Floor Execution Flow
// Main.java
public class Main {
public static void main(String[] args) {
ParkingLot parkingLot = new ParkingLot();
// Initialize spots
parkingLot.addParkingSpot(new CarParkingSpot("C1"));
parkingLot.addParkingSpot(new CarParkingSpot("C2"));
parkingLot.addParkingSpot(new BikeParkingSpot("B1"));
// Create Vehicles
Vehicle car = VehicleFactory.createVehicle(VehicleType.CAR, "CAR-9999");
Vehicle bike = VehicleFactory.createVehicle(VehicleType.BIKE, "BIKE-1111");
// Entry
Ticket carTicket = parkingLot.entry(car);
Ticket bikeTicket = parkingLot.entry(bike);
// Exit after 120 minutes (Simulated)
if (carTicket != null) {
PaymentStrategy cardPayment = new CreditCardPayment("1234-5678-9012-3456");
parkingLot.exit(carTicket, cardPayment, 120); // 2 hours -> Should cost $40.0
}
}
}
10. Extensibility: Multi-Floor Parking & The Builder Pattern
In real-world scenarios, parking lots span multiple floors, each with unique layouts. We can extend our system by introducing a ParkingFloor entity and using the Builder Pattern to construct a customized ParkingLot.
A. The Parking Floor Entity
// ParkingFloor.java
import java.util.ArrayList;
import java.util.List;
public class ParkingFloor {
private final int floorNumber;
private final List<ParkingSpot> spots;
public ParkingFloor(int floorNumber) {
this.floorNumber = floorNumber;
this.spots = new ArrayList<>();
}
public void addParkingSpot(ParkingSpot spot) {
spots.add(spot);
}
public ParkingSpot findAvailableSpot(VehicleType type) {
for (ParkingSpot spot : spots) {
if (!spot.isOccupied() && spot.getSupportedType() == type) {
return spot;
}
}
return null;
}
public int getFloorNumber() { return floorNumber; }
public List<ParkingSpot> getSpots() { return spots; }
}
B. Multi-Floor Parking Lot
// MultiFloorParkingLot.java
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
public class MultiFloorParkingLot {
private final List<ParkingFloor> floors;
public MultiFloorParkingLot(List<ParkingFloor> floors) {
this.floors = floors;
}
public synchronized Ticket entry(Vehicle vehicle) {
for (ParkingFloor floor : floors) {
ParkingSpot spot = floor.findAvailableSpot(vehicle.getVehicleType());
if (spot != null) {
spot.parkVehicle(vehicle);
String ticketId = UUID.randomUUID().toString();
System.out.println("Vehicle " + vehicle.getLicensePlate() +
" parked on Floor " + floor.getFloorNumber() +
" at Spot " + spot.getSpotNumber());
return new Ticket(ticketId, vehicle, spot);
}
}
System.out.println("No spots available across any floor for: " + vehicle.getVehicleType());
return null;
}
public synchronized void exit(Ticket ticket, PaymentStrategy paymentStrategy, long durationMinutes) {
ParkingSpot spot = ticket.getSpot();
double fee = ticket.getVehicle().getFeeStrategy().calculateFee(durationMinutes);
if (paymentStrategy.processPayment(fee)) {
spot.vacate();
System.out.println("Spot " + spot.getSpotNumber() + " vacated successfully.");
}
}
}
C. The Parking Lot Builder
// ParkingLotBuilder.java
import java.util.ArrayList;
import java.util.List;
public class ParkingLotBuilder {
private final List<ParkingFloor> floors = new ArrayList<>();
public ParkingLotBuilder addFloor(int floorNumber, int carSpotCount, int bikeSpotCount) {
ParkingFloor floor = new ParkingFloor(floorNumber);
for (int i = 1; i <= carSpotCount; i++) {
floor.addParkingSpot(new CarParkingSpot("F" + floorNumber + "-CAR-" + i));
}
for (int i = 1; i <= bikeSpotCount; i++) {
floor.addParkingSpot(new BikeParkingSpot("F" + floorNumber + "-BIKE-" + i));
}
floors.add(floor);
return this;
}
public MultiFloorParkingLot build() {
return new MultiFloorParkingLot(floors);
}
}
Multi-Floor Execution Flow
// MultiFloorMain.java
public class MultiFloorMain {
public static void main(String[] args) {
// Build a multi-floor parking lot using the Builder Pattern
MultiFloorParkingLot multiFloorLot = new ParkingLotBuilder()
.addFloor(1, 2, 5) // Floor 1: 2 Car spots, 5 Bike spots (Lower floor optimized for bikes)
.addFloor(2, 10, 2) // Floor 2: 10 Car spots, 2 Bike spots (Upper floor optimized for cars)
.build();
Vehicle heavyCar = VehicleFactory.createVehicle(VehicleType.CAR, "MH-12-AB-1234");
// Entry
Ticket ticket = multiFloorLot.entry(heavyCar);
// Exit
if (ticket != null) {
multiFloorLot.exit(ticket, new CashPayment(), 180); // 3 Hours
}
}
}
11. Production Realities: Concurrency, Complexity, & Debugging
In production systems, simple sequential iterations do not scale. We must address real-world engineering trade-offs:
A. Time Complexity Optimization
In our naive implementation, finding an available spot requires iterating through all spots:
- Naive Search: where is the total number of spots.
To optimize this to or , we can maintain separate data structures for free and occupied spots:
- Min-Heap (Priority Queue): To always allocate the closest spot (e.g., lowest floor, lowest spot number), we can store free spots in a
PriorityQueue. Finding a spot becomes (peek) and updating it becomes . - HashMap: To look up a spot by its ID during vacation, we can use a
Map<String, ParkingSpot>, reducing lookup time to .
B. Concurrency & Thread Safety
In a busy parking lot with multiple entry and exit gates, multiple threads will attempt to book or vacate spots simultaneously.
- Synchronized Methods: Using
synchronizedon theentryandexitmethods ofParkingLotlocks the entire lot, creating a performance bottleneck. - Fine-Grained Locking: Instead of locking the entire lot, lock only the specific
ParkingFlooror use aReentrantLockper vehicle type category to allow concurrent entries for cars and bikes.
C. Handling Illegal Actions
- Double Parking Prevention: The
parkVehiclemethod inParkingSpotis markedsynchronizedand checks theisOccupiedflag to prevent race conditions where two gates attempt to assign the same spot to different vehicles. - Payment Validation: The system must prevent a vehicle from vacating a spot if the payment transaction fails. This is enforced by nesting
spot.vacate()inside the successful execution block ofpaymentStrategy.processPayment(fee).
12. Summary and Key Takeaways
- Decouple Dynamic Behaviors: Use the Strategy Pattern to isolate pricing algorithms and payment processing from physical entities like
VehicleorParkingSpot. - Bottom-Up Composition: Build and test low-level components (Strategies, Vehicles) before composing them into high-level orchestrators (Floors, Lots).
- Leverage Creational Patterns: Use the Factory Pattern to standardize vehicle creation and the Builder Pattern to construct complex, multi-floor parking lot configurations.
- Optimize for Scale: Transition from naive list traversals to or lookups using specialized data structures like
HashMapandPriorityQueuein production. - Design for Concurrency: Protect shared physical resources from race conditions by implementing fine-grained locking mechanisms at the floor or spot level.