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.

August 10, 2025·18 min read

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#

EntityResponsibility
ParkingLotSingleton facade — orchestrates levels, ticketing, payment
ParkingLevelRepresents one floor; finds available spots
ParkingSpotA single spot — tracks type, occupancy, and parked vehicle
VehicleAbstract base class; Car, Bike, Truck extend it
ParkingLotTicketIssued on entry; tracks vehicle, spot, entry time, status
TicketInventoryThread-safe store for all active/completed tickets
PaymentServiceCalculates duration and delegates pricing to a strategy
PaymentStrategyInterface; 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.

Singleton Pattern Structure

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.

Facade Pattern Structure

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.

Strategy Pattern Structure

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().

Builder Pattern Structure

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.

java
public enum VehicleType {
    CAR, BIKE, TRUCK
}

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.

java
public enum TicketStatus {
    ACTIVE, COMPLETED
}

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.

java
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#

java
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#

Parking Lot 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.