🎯 Objective
Design a modular and extensible Parking Lot System that supports:
- Multiple parking floors
- Vehicle types: Car, Bike, Truck
- Parking slots of different sizes
- Entry/Exit Gates
- Ticket generation and payment handling
- Real-time slot availability
We'll approach this LLD as if in a real interview, building the system iteratively, extracting entities, responsibilities, and applying OOP principles, SOLID, and design patterns for long-term maintainability.
đź§ Step 1: Understanding and Modeling the Domain
Let’s whiteboard core domain concepts through real-world storytelling:
“Imagine you pull into a mall with your car. At the entrance gate, the system detects your vehicle, assigns a slot, and prints a ticket. You later exit, scan the ticket, pay the amount based on duration, and leave.”
From this flow, we extract the key nouns → candidate entities:
🎯 Initial Entity Candidates:
- Vehicle
- ParkingSlot
- ParkingFloor
- ParkingLot
- Gate (Entry/Exit)
- Ticket
- Payment
- DisplayBoard (shows available slots)
🔍 Step 2: Define Core Abstractions
Let’s now define base classes/interfaces with clean responsibilities.
âś… 2.1 Vehicle
— Entity
public abstract class Vehicle {
private final String licensePlate;
private final VehicleType type;
public Vehicle(String licensePlate, VehicleType type) {
this.licensePlate = licensePlate;
this.type = type;
}
// Getters
}
âś… 2.2 VehicleType
— Enum
public enum VehicleType {
CAR, BIKE, TRUCK
}
âś… 2.3 ParkingSlot
— Entity with SRP
public class ParkingSlot {
private final String id;
private final VehicleType supportedType;
private boolean isOccupied;
public ParkingSlot(String id, VehicleType supportedType) {
this.id = id;
this.supportedType = supportedType;
}
public boolean canFitVehicle(Vehicle vehicle) {
return !isOccupied && supportedType == vehicle.getType();
}
public void assignVehicle() { isOccupied = true; }
public void releaseVehicle() { isOccupied = false; }
// Getters
}
âś… 2.4 ParkingFloor
— Aggregates Slots
public class ParkingFloor {
private final String name;
private final List<ParkingSlot> slots;
public ParkingFloor(String name) {
this.name = name;
this.slots = new ArrayList<>();
}
public Optional<ParkingSlot> getAvailableSlot(Vehicle vehicle) {
return slots.stream().filter(slot -> slot.canFitVehicle(vehicle)).findFirst();
}
public void addSlot(ParkingSlot slot) {
slots.add(slot);
}
}
Extensibility Note: ParkingFloor doesn’t care about vehicle logic; it just aggregates and filters slots — single responsibility preserved.
âś… 2.5 ParkingLot
— System Coordinator
public class ParkingLot {
private final List<ParkingFloor> floors;
private final List<Gate> entryGates;
private final List<Gate> exitGates;
public ParkingLot(List<ParkingFloor> floors, List<Gate> entryGates, List<Gate> exitGates) {
this.floors = floors;
this.entryGates = entryGates;
this.exitGates = exitGates;
}
public Optional<ParkingSlot> findSlotForVehicle(Vehicle vehicle) {
for (ParkingFloor floor : floors) {
Optional<ParkingSlot> slot = floor.getAvailableSlot(vehicle);
if (slot.isPresent()) return slot;
}
return Optional.empty();
}
}
🏷️ Step 3: Ticket Generation Logic
âś… 3.1 Ticket
— Value Object
public class Ticket {
private final String ticketId;
private final Vehicle vehicle;
private final ParkingSlot slot;
private final LocalDateTime entryTime;
public Ticket(String ticketId, Vehicle vehicle, ParkingSlot slot) {
this.ticketId = ticketId;
this.vehicle = vehicle;
this.slot = slot;
this.entryTime = LocalDateTime.now();
}
// Getters
}
âś… 3.2 Gate
— Factory-like Role
public abstract class Gate {
protected final String gateId;
public Gate(String gateId) { this.gateId = gateId; }
}
public class EntryGate extends Gate {
private final ParkingLot lot;
public EntryGate(String gateId, ParkingLot lot) {
super(gateId);
this.lot = lot;
}
public Optional<Ticket> generateTicket(Vehicle vehicle) {
Optional<ParkingSlot> slotOpt = lot.findSlotForVehicle(vehicle);
if (slotOpt.isEmpty()) return Optional.empty();
ParkingSlot slot = slotOpt.get();
slot.assignVehicle();
return Optional.of(new Ticket(UUID.randomUUID().toString(), vehicle, slot));
}
}
đź’° Step 4: Payment and Exit Workflow
âś… 4.1 Payment
— Entity
public class Payment {
private final String paymentId;
private final double amount;
private final LocalDateTime paidAt;
private final PaymentStatus status;
public Payment(double amount) {
this.paymentId = UUID.randomUUID().toString();
this.amount = amount;
this.paidAt = LocalDateTime.now();
this.status = PaymentStatus.SUCCESS;
}
}
âś… 4.2 ExitGate
— Checkout Logic
public class ExitGate extends Gate {
public ExitGate(String gateId) { super(gateId); }
public Payment processExit(Ticket ticket) {
long duration = Duration.between(ticket.getEntryTime(), LocalDateTime.now()).toMinutes();
double ratePerHour = 20.0;
double amount = Math.ceil(duration / 60.0) * ratePerHour;
ticket.getSlot().releaseVehicle();
return new Payment(amount);
}
}
đź§± Step 5: Design Principles Applied
Principle | Applied Where & How |
---|---|
S — SRP |
Ticket , ParkingSlot , Gate all have single clear responsibilities |
O — OCP | Vehicle types, payment strategies can be extended via enums or strategy pattern |
L — LSP | Entry/ExitGate are substitutable for Gate |
I — ISP | Interfaces like PaymentProcessor can be introduced for online/card/cash payments |
D — DIP | Payment service can be injected via abstraction |
đź§° Optional Extensibility Add-ons (Post-MVP)
Feature | Design Approach |
---|---|
Online Payment | Strategy Pattern for PaymentProcessor
|
Admin Dashboard | Observer Pattern on ParkingLot availability |
Slot Allocation Algorithm | Use Strategy pattern to support “closest slot”, “VIP slot”, etc. |
Pricing Rules by Time | Decorator/Strategy for PricingEngine
|
âś… Final Thoughts
This walkthrough demonstrates how to incrementally evolve a complex system while adhering to:
- Domain modeling best practices
- SOLID and OOP design
- Extensibility-first mindset
- Clean code boundaries
👉 A well-executed LLD interview is less about syntax and more about narrative clarity, logical separation of concerns, and engineering foresight.
Top comments (0)