Low Level Design
Design a Parking Lot
A complete low-level design walkthrough for a multi-level parking lot system — from requirements clarification to class diagrams, design patterns, and thread-safe Java implementation.
Problem Description#
Design a Parking Lot system that manages vehicle entry, spot assignment, ticketing, and payment across multiple levels.
At its core, a parking lot is a resource-management problem:
- Vehicles arrive and need a compatible spot
- Spots have types (bike, car, truck)
- Tickets track occupancy and time
- Payment is calculated on exit
This is a classic LLD interview problem because it touches OOP fundamentals, multiple design patterns, and concurrency — all in a realistic setting.
Clarify Requirements#
Before designing anything, ask these questions in an interview:
Functional
- How many levels does the parking lot have? Are levels fixed or dynamic?
- What vehicle types should be supported? (bike, car, truck?)
- Can a car park in a truck spot, or only an exact-fit spot?
- How is payment calculated — hourly, per minute, flat rate?
- Does the system need to issue a physical ticket or just a digital record?
- Should entry/exit support multiple simultaneous vehicles?
Non-functional
- How many concurrent vehicles are expected?
- Is real-time availability tracking required?
- Should the system support multiple parking lots (multi-location)?
Final Requirements#
After clarification, here's what we'll build:
- The parking lot has multiple levels, each with a fixed number of spots
- Supported vehicle types: Car, Bike, Truck — each requires an exact-match spot type
- On entry: find an available compatible spot, issue a ticket (with entry time + spot), mark spot occupied
- On exit: validate ticket, calculate fee using a pluggable strategy, process payment, vacate spot
- Real-time availability is tracked per spot (occupied/free)
- Support concurrent entry/exit — the system must be thread-safe
Core Entities#
| Entity | Responsibility |
|---|---|
| ParkingLot | Singleton facade — orchestrates levels, ticketing, payment |
| ParkingLevel | Represents one floor; finds available spots |
| ParkingSpot | A single spot — tracks type, occupancy, and parked vehicle |
| Vehicle | Abstract base class; Car, Bike, Truck extend it |
| ParkingLotTicket | Issued on entry; tracks vehicle, spot, entry time, status |
| TicketInventory | Thread-safe store for all active/completed tickets |
| PaymentService | Calculates duration and delegates pricing to a strategy |
| PaymentStrategy | Interface; HourlyPaymentStrategy / MinutePaymentStrategy |
Patterns Used#
1. Singleton — ParkingLot#
There should be exactly one ParkingLot instance managing the entire structure. We use a synchronized getInstance() to ensure thread-safe lazy initialization.

2. Facade — ParkingLot#
The ParkingLot class (in the facade package) hides all internal complexity — levels, spots, ticketing, and payment — behind two clean methods: entry() and exit(). Callers never interact with ParkingLevel, TicketInventory, or PaymentService directly.

3. Strategy — PaymentStrategy#
Pricing logic is decoupled from the system. HourlyPaymentStrategy charges $2/hour (rounded up); MinutePaymentStrategy charges $0.05/minute. Swap pricing without touching core logic.

4. Builder — ParkingLotTicket.Builder#
Tickets have multiple optional fields and are immutable after creation. The Builder provides a fluent API and auto-generates the UUID in build().

5. Repository — TicketInventory#
Abstracts ticket storage and lookup, keeping persistence concerns out of the facade. Uses ConcurrentHashMap with synchronized methods for thread safety.
6. Inheritance / Template Method — Vehicle#
Vehicle is abstract, providing common fields (licensePlate, vehicleType) and methods. Car, Bike, and Truck only supply their specific VehicleType to the parent constructor.
Code#
Infrastructure — Enums, Vehicles, Spots#
The foundational types: enumerations that define compatible pairs, an abstract vehicle hierarchy, and the spot/level structures that track physical availability.
public enum VehicleType {
CAR, BIKE, TRUCK
}public enum SpotType {
CAR(VehicleType.CAR),
BIKE(VehicleType.BIKE),
TRUCK(VehicleType.TRUCK);
private final VehicleType compatibleVehicleType;
SpotType(VehicleType compatibleVehicleType) {
this.compatibleVehicleType = compatibleVehicleType;
}
public boolean canFitVehicle(VehicleType vehicleType) {
return this.compatibleVehicleType == vehicleType;
}
}public abstract class Vehicle {
protected String licensePlate;
protected VehicleType vehicleType;
public Vehicle(String licensePlate, VehicleType type) {
this.licensePlate = licensePlate;
this.vehicleType = type;
}
public VehicleType getVehicleType() { return vehicleType; }
public String getLicenseNumber() { return licensePlate; }
}public class Car extends Vehicle {
public Car(String plate) { super(plate, VehicleType.CAR); }
}public class Bike extends Vehicle {
public Bike(String plate) { super(plate, VehicleType.BIKE); }
}public class Truck extends Vehicle {
public Truck(String plate) { super(plate, VehicleType.TRUCK); }
}public class ParkingSpot {
private int spotId;
private SpotType spotType;
private boolean isOccupied;
private Vehicle parkedVehicle;
public ParkingSpot(SpotType spotType, int spotId) {
this.isOccupied = false;
this.spotId = spotId;
this.spotType = spotType;
}
public void occupySpot(Vehicle vehicle) {
this.parkedVehicle = vehicle;
this.isOccupied = true;
}
public void vacateSpot() {
this.parkedVehicle = null;
this.isOccupied = false;
}
public boolean isOccupied() { return isOccupied; }
public SpotType getSpotType() { return spotType; }
public int getSpotId() { return spotId; }
}import java.util.List;
public class ParkingLevel {
private int levelId;
private List<ParkingSpot> parkingSpots;
public ParkingLevel(int levelId, List<ParkingSpot> spots) {
this.levelId = levelId;
this.parkingSpots = spots;
}
public ParkingSpot findParkingSpot(VehicleType type) {
for (ParkingSpot spot : parkingSpots) {
if (!spot.isOccupied() && spot.getSpotType().canFitVehicle(type)) {
return spot;
}
}
return null;
}
public boolean isFull() {
for (ParkingSpot spot : parkingSpots) {
if (!spot.isOccupied()) return false;
}
return true;
}
}Ticketing & Payment#
ParkingLotTicket is built with a fluent Builder and becomes immutable after build(). TicketInventory stores active tickets in a ConcurrentHashMap. PaymentStrategy keeps pricing logic swappable without touching ParkingLot.
public enum TicketStatus {
ACTIVE, COMPLETED
}import java.time.LocalTime;
import java.util.UUID;
public class ParkingLotTicket {
private final UUID ticketId;
private final Vehicle vehicle;
private final ParkingSpot parkingSpot;
private final LocalTime entryTime;
private TicketStatus status;
private ParkingLotTicket(Builder builder) {
this.ticketId = builder.ticketId;
this.vehicle = builder.vehicle;
this.parkingSpot = builder.parkingSpot;
this.entryTime = builder.entryTime;
this.status = TicketStatus.ACTIVE;
}
public UUID getTicketId() { return ticketId; }
public Vehicle getVehicle() { return vehicle; }
public ParkingSpot getParkingSpot(){ return parkingSpot; }
public LocalTime getEntryTime() { return entryTime; }
public TicketStatus getStatus() { return status; }
public void markCompleted() { this.status = TicketStatus.COMPLETED; }
public static class Builder {
private UUID ticketId;
private Vehicle vehicle;
private ParkingSpot parkingSpot;
private LocalTime entryTime;
public Builder vehicle(Vehicle v) { this.vehicle = v; return this; }
public Builder parkingSpot(ParkingSpot s) { this.parkingSpot = s; return this; }
public Builder entryTime(LocalTime t) { this.entryTime = t; return this; }
public ParkingLotTicket build() {
if (vehicle == null || parkingSpot == null || entryTime == null)
throw new IllegalStateException("vehicle, parkingSpot, and entryTime are required");
this.ticketId = UUID.randomUUID();
return new ParkingLotTicket(this);
}
}
}import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
public class TicketInventory {
private final Map<UUID, ParkingLotTicket> tickets = new ConcurrentHashMap<>();
public synchronized void addTicket(ParkingLotTicket ticket) {
tickets.put(ticket.getTicketId(), ticket);
}
public synchronized ParkingLotTicket getTicketById(UUID ticketId) {
return tickets.get(ticketId);
}
}public interface PaymentStrategy {
double calculate(long durationInMinutes);
}// $2.00 / hour, rounds up partial hours
public class HourlyPaymentStrategy implements PaymentStrategy {
private static final double HOURLY_RATE = 2.0;
@Override
public double calculate(long duration) {
long hours = duration / 60;
if (duration % 60 != 0) hours++;
return HOURLY_RATE * hours;
}
}// $0.05 / minute
public class MinutePaymentStrategy implements PaymentStrategy {
private static final double MINUTE_RATE = 0.05;
@Override
public double calculate(long duration) {
return MINUTE_RATE * duration;
}
}import java.time.LocalTime;
import java.time.temporal.ChronoUnit;
public class PaymentService {
public double calculatePaymentAmount(
ParkingLotTicket ticket,
PaymentStrategy strategy,
LocalTime exitTime) {
long minutes = ticket.getEntryTime().until(exitTime, ChronoUnit.MINUTES);
return strategy.calculate(minutes);
}
public boolean processPayment(double amount, ParkingLotTicket ticket) {
System.out.printf("Payment of $%.2f processed for ticket %s%n",
amount, ticket.getTicketId());
return true;
}
}ParkingLot — Singleton Facade#
Orchestrates everything: resolves spots across levels, issues tickets, and processes exits. All spot mutations are guarded with synchronized to prevent double-assignment under concurrent load.
import java.time.LocalTime;
import java.util.*;
public class ParkingLot {
private static ParkingLot instance;
private final List<ParkingLevel> parkingLevels;
private final PaymentStrategy strategy;
private final TicketInventory ticketInventory;
private final PaymentService paymentService;
private ParkingLot(PaymentStrategy strategy, int levels) {
this.strategy = strategy;
this.ticketInventory = new TicketInventory();
this.paymentService = new PaymentService();
this.parkingLevels = initializeLevels(levels);
}
public static synchronized ParkingLot getInstance(PaymentStrategy strategy, int levels) {
if (instance == null) instance = new ParkingLot(strategy, levels);
return instance;
}
private List<ParkingLevel> initializeLevels(int totalLevels) {
List<ParkingLevel> levels = new ArrayList<>();
for (int i = 0; i < totalLevels; i++) {
List<ParkingSpot> spots = new ArrayList<>();
int id = 0;
spots.add(new ParkingSpot(SpotType.BIKE, id++));
spots.add(new ParkingSpot(SpotType.BIKE, id++));
spots.add(new ParkingSpot(SpotType.CAR, id++));
spots.add(new ParkingSpot(SpotType.CAR, id++));
spots.add(new ParkingSpot(SpotType.CAR, id++));
spots.add(new ParkingSpot(SpotType.TRUCK, id));
levels.add(new ParkingLevel(i, spots));
}
return levels;
}
// Entry: find spot → occupy → issue ticket
public ParkingLotTicket entry(Vehicle vehicle, LocalTime entryTime) {
ParkingSpot spot;
synchronized (this) {
spot = findAvailableSpot(vehicle.getVehicleType());
if (spot == null) {
System.out.println("No spot available for: " + vehicle.getLicenseNumber());
return null;
}
spot.occupySpot(vehicle);
}
ParkingLotTicket ticket = new ParkingLotTicket.Builder()
.vehicle(vehicle)
.parkingSpot(spot)
.entryTime(entryTime)
.build();
ticketInventory.addTicket(ticket);
return ticket;
}
// Exit: validate ticket → calculate fee → pay → vacate spot
public void exit(LocalTime exitTime, UUID ticketId) {
ParkingLotTicket ticket = ticketInventory.getTicketById(ticketId);
if (ticket == null) throw new IllegalArgumentException("Invalid ticket ID");
double amount = paymentService.calculatePaymentAmount(ticket, strategy, exitTime);
paymentService.processPayment(amount, ticket);
synchronized (this) {
ticket.getParkingSpot().vacateSpot();
}
ticket.markCompleted();
}
public ParkingSpot findAvailableSpot(VehicleType vehicleType) {
for (ParkingLevel level : parkingLevels) {
ParkingSpot spot = level.findParkingSpot(vehicleType);
if (spot != null) return spot;
}
return null;
}
}
Demo#
ParkingLot lot = ParkingLot.getInstance(new HourlyPaymentStrategy(), 4);
Vehicle car = new Car("KA-01-AB-1234");
ParkingLotTicket ticket = lot.entry(car, LocalTime.now());
if (ticket != null) {
// ... vehicle parks for 2 hours ...
lot.exit(LocalTime.now().plusHours(2), ticket.getTicketId());
// Output: Payment of $4.00 processed for ticket <uuid>
}
Class Diagram#
System Representation#

Extendible — Follow Ups#
1. Support for EV charging spots#
Add a CHARGING_CAR variant to SpotType and update canFitVehicle logic. No changes to ParkingLot or ParkingLevel needed.
2. Dynamic pricing (peak/off-peak)#
Introduce a TimeAwarePaymentStrategy that wraps an existing strategy and applies a multiplier based on the time of day. The PaymentService already delegates to the strategy — swap it at runtime.
3. Spot reservation#
Add a ReservedParkingSpot subclass or a reserved flag on ParkingSpot. Expose a reserve(Vehicle, LocalTime) method on ParkingLot that pre-assigns a spot.
4. Multi-location support#
Remove the static singleton and introduce a ParkingLotRegistry that manages named ParkingLot instances. Each lot remains independent.
5. Fix LocalTime → LocalDateTime#
The current implementation uses LocalTime, which breaks for stays crossing midnight. Replace both entryTime and exitTime with LocalDateTime (or Instant) — a simple field-type change with no structural impact.
6. Persistent ticket archival#
TicketInventory keeps all tickets in memory. In production, completed tickets should be moved to a database. Introduce a TicketRepository interface with InMemoryTicketRepository and DatabaseTicketRepository implementations — no changes to ParkingLot required.