Low Level Design: Parking Lot System

December 19, 2025

Build a comprehensive solution for efficiently managing a parking lot. The system should automate key processes such as vehicle entry, ticket generation, intelligent spot allocation, real-time occupancy tracking, and seamless vehicle exit, ensuring a smooth and reliable parking experience.

parking-lot


Functional Requirements

  1. Support parking for motorcycles, cars, and trucks.

  2. Maintain three spot types (compact, regular, oversized) with appropriate capacity.

  3. Assign spots based on vehicle size, where a given vehicle type may be compatible with multiple spot types. For example, a car can be accommodated in either a compact spot or a regular spot.

  4. Calculate fees using spot type and parking duration, with time-of-day rate variations.



Primary Use Cases

The parking lot system operates around two primary workflows: vehicle entry and vehicle exit. Here's how each process works:

Vehicle Entry

  1. A vehicle (e.g., motorcycle, car, truck) arrives at the entry point.

  2. The parking lot system captures essential details such as the license number, vehicle type, and the arrival time, and finds an appropriate available parking spot (e.g., compact, regular, or oversized) based on the vehicle type.

  3. If a suitable spot is available, the system assigns the parking spot to the vehicle, generates a ticket, and returns the corresponding ticket Id to the user.

Vehicle Exit

  1. The vehicle arrives at the exit gate and provides the ticket Id to the parking lot system.

  2. The system retrieves the corresponding ticket and calculates the parking fee based on the duration of the stay.

  3. After the payment is completed, the system marks the associated parking spot as available, updates the ticket, and allows the vehicle to exit.


Design Rationale

The parking lot acts as the central orchestrator of the system, coordinating all core operations such as finding available spots, generating tickets, calculating fees, and processing payments.

However, handling all these operations within a single class leads to a violation of the Single Responsibility Principle (SRP). To address this, the ParkingLot class can be designed as a facade, delegating responsibilities to specialized components.

// ✅ Good design

ParkingLot  SpotManager        : find spot, free spot 
ParkingLot  TicketManager      : create ticket, fetch ticket
ParkingLot  PriceCalculator    : calculate fee  
ParkingLot  PaymentProcessor   : process payment 

This design promotes a clear separation of concerns by ensuring that each component is responsible for a specific piece of functionality.


Sequence Flow

Let's discuss the step-by-step interactions between different system components for the primary use cases, highlighting how responsibilities are coordinated to achieve the desired functionality.

Vehicle Entry (Park Vehicle)

  1. The Vehicle arrives and sends a parking request to the ParkingLot system, along with its details (licenseNumber).

  2. The ParkingLot system:

    • Calls the SpotManager to findAvailableSpot(Vehicle) based on vehicle's allowed List<SpotType>.

    • If a spot is found, the ParkingLot system marks the ParkingSpot as occupied and calls the TicketManager to createTicket(Vehicle, ParkingSpot).

  3. Returns the generated ticketId to the user. Otherwise, the request is rejected if no spot is available.


park-vehicle-sequence-flow-diagram


Vehicle Exit (Unpark Vehicle)

  1. The Vehicle arrives at the exit and provides the ticketId to the ParkingLot system.

  2. The ParkingLot system:

    • Calls the TicketManager to getTicket(ticketId) and passes it to the PriceCalculator to calculateFee(Ticket) based on the duration of the stay.

    • Retrieves the ParkingSpot associated with the Ticket and instructs the SpotManager to freeSpot(ParkingSpot).

    • Updates the Ticket with the exitTime and calculated amount.

  3. The parking fee is returned, and the Vehicle is allowed to exit.


unpark-vehicle-mermaid-diagram



Low Level Design

Let's discuss the low-level design and complexities for each functionality. This will help us build a system that is efficient, scalable, and optimized for real-world usage.

ParkingLot

public class ParkingLot {
    private SpotManager SpotManager;
    private TicketManager ticketManager;
    private PriceCalculator priceCalculator;

    public ParkingLot(SpotManager spotManager,
                        TicketManager ticketManager,
                        PriceCalculator priceCalculator) {
        this.spotManager = spotManager;
        this.ticketManager = ticketManager;
        this.priceCalculator = priceCalculator;
    }

    public Ticket parkVehicle(Vehicle vehicle) {
        ParkingSpot spot = spotManager.findAvailableSpot(vehicle);
        if (spot == null) throw new RuntimeException("No spot available");

        spot.assignVehicle(vehicle);
        Ticket ticket = ticketManager.generateTicket(vehicle, spot);
        return ticket;
    }

    public double unparkVehicle(String ticketId) {
        Ticket ticket = ticketManager.retrieveTicket(ticketId);
        if (ticket == null) throw new RuntimeException("Invalid ticket Id");

        double fee = priceCalculator.calculateFee(ticket);

        ParkingSpot spot = ticket.getSpot();
        spot.removeVehicle();
        spotManager.freeParkingSpot(spot);

        ticket.closeTicket(LocalDateTime.now(), fee);
        return fee;
    }
}

SpotManager

The SpotManager class is responsible for managing parking spot allocation, including handling queries such as finding an available ParkingSpot for a given vehicle type.

Since any given vehicle type can be accomodated in multiple SpotType, it is efficient to maintain a Map<SpotType, List<ParkingSpot>>, grouping parking spots by their type. However, if all spots (both free and occupied) are stored together, we would need to traverse the entire list each time to find an available spot, resulting in an O(n) lookup.

To optimize this, we can maintain two separate maps: availableSpots (free spots) and occupiedSpots (in-use spots). When a vehicle arrives, the system only checks the availableSpots map for the relevant SpotType, eliminating the need to scan all spots. This improves the lookup time from O(n) to near O(1) for spot allocation.

public class SpotManager {
    private Map<SpotType, List<ParkingSpot>> availableSpots = new HashMap<>();
    private Map<SpotType, List<ParkingSpot>> occupiedSpots = new HashMap<>();

    public SpotManager() {
        for(SpotType type: SpotType.values()) {
            availableSpots.put(type, new ArrayList<>());
            occupiedSpots.put(type, new ArrayList<>());
        }
    }

    public void addSpot(ParkingSpot spot) {
        availableSpots.get(spot.getType()).add(spot);
    }

    public ParkingSpot findAvailableSpot(Vehicle vehicle) {
        List<SpotType> allowedTypes = vehicle.getAllowedSpotTypes();

        for (SpotType type : allowedTypes) {    
            List<ParkingSpot> spots = availableSpots.get(type);
            if (!spots.isEmpty()) {
                ParkingSpot spot = spots.remove(0);
                occupiedSpots.get(type).add(spot);
                return spot;
            }
        }
        return null;
    }

    public void freeSpot(ParkingSpot spot) {
        occupiedSpots.get(spot.getType()).remove(spot);
        availableSpots.get(spot.getType()).add(spot);
    }

    /* Bad Design - Violates the Open-Closed Principle. Instead of writing 
                    conditional logic, each vehicle should define its own
                    parking rules.

    public List<SpotType> getAllowedSpotTypes(VehicleType type) {
        switch (type) {
            case MOTORCYCLE:
                return List.of(SpotType.COMPACT);
            case CAR:
                return List.of(SpotType.COMPACT, SpotType.REGULAR);
            case TRUCK:
                return List.of(SpotType.OVERSIZED);
        }
        return new ArrayList<>();
    }
    */
}

Vehicle

The Vehicle class represents any vehicle entering the parking lot and serves as an abstract base class for all specific vehicle types such as Motorcycle, Car, and Truck.

abstract class Vehicle {
    protected String licenseNumber; // accessible within subclasses, even in different packages

    public Vehicle(String licenseNumber) {
        this.licenseNumber = licenseNumber;
    }

    public abstract List<SpotType> getAllowedSpotTypes();
}

class Motorcycle extends Vehicle {
    public List<SpotType> getAllowedSpotTypes() {
        return List.of(SpotType.COMPACT);
    }
}

class Car extends Vehicle {
    public List<SpotType> getAllowedSpotTypes() {
        return List.of(SpotType.COMPACT, SpotType.REGULAR);
    }
}

class Truck extends Vehicle {
    public List<SpotType> getAllowedSpotTypes() {
        return List.of(SpotType.OVERSIZED);
    }
}

ParkingSpot

The ParkingSpot class represents an individual parking space within the parking lot. Each parking spot supports basic operations to manage its occupancy, including assigning a Vehicle when occupied and removing the Vehicle when it becomes available.

// ================= ENUMS =================
public enum SpotType {

    COMPACT(10),
    REGULAR(20),
    OVERSIZED(30);

    private final double baseRate;

    SpotType(double baseRate) {
        this.baseRate = baseRate;
    }

    public double getBaseRate() {
        return baseRate;
    }
}

// ================= PARKING SPOT =================
public class ParkingSpot {
    private String spotId;
    private SpotType type;
    private boolean isOccupied;
    private Vehicle vehicle;

    public ParkingSpot(String spotId, SpotType type) {
        this.spotId = spotId;
        this.type = type;
        this.isOccupied = false;
    }

    public boolean isAvailable() {
        return !isOccupied;
    }

    public void assignVehicle(Vehicle vehicle) {
        this.vehicle = vehicle;
        this.isOccupied = true;
    }

    public void removeVehicle() {
        this.vehicle = null;
        this.isOccupied = false;
    }

    public SpotType getType() {
        return type;
    }
}

TicketManager

The TicketManager class encapsulates the logic for generating and managing parking tickets.

During the parking process, it generates a new Ticket by associating the Vehicle with a ParkingSpot and returns a unique ticketId. During the unparking process, it uses the ticketId to retrieve the corresponding Ticket, which contains all relevant parking details.

To enable efficient retrieval, the TicketManager maintains a Map<ticketId, Ticket>, allowing direct, constant-time access to tickets without scanning through all records.

public class TicketManager {
    private Map<String, Ticket> ticketMap;

    public TicketManager() {
        this.ticketMap = new HashMap<>();
    }

    // Create ticket during parking
    public Ticket createTicket(Vehicle vehicle, ParkingSpot spot) {
        Ticket ticket = new Ticket(vehicle, spot);
        ticketMap.put(ticket.getTicketId(), ticket);
        return ticket();
    }

    // Fetch ticket during unparking
    public Ticket getTicket(String ticketId) {
        Ticket ticket = ticketMap.get(ticketId);
        if (ticket == null) {
            throw new RuntimeException("Invalid ticketId");
        }
        return ticket;
    }
}

Ticket

The Ticket class represents a parking session and acts as a record of a vehicle's entry and exit within the parking lot. It captures essential details such as associated Vehicle, assigned ParkingSpot, entryTime, and exitTime, along with the total parking amount.

It primarily acts as a data holder with minimal behavior, such as updating the exitTime and amount.

public class Ticket {
    private String ticketId;
    private Vehicle vehicle;
    private ParkingSpot parkingSpot;
    private LocalDateTime entryTime;
    private LocalDateTime exitTime;
    private double amount;

    public Ticket(Vehicle vehicle, ParkingSpot spot) {
        this.ticketId = UUID.randomUUID().toString();
        this.vehicle = vehicle;
        this.spot = spot;
        this.entryTime = LocalDateTime.now();
    }

    public String getTicketId() {
        return ticketId;
    }

    public LocalDateTime getEntryTime() {
        return entryTime;
    }

    public void closeTicket(LocalDateTime exitTime, double amount) {
        this.exitTime = exitTime;
        this.amount = amount;
    }

    public ParkingSpot getSpot() {
        return spot;
    }
}

PriceCalculator

The PriceCalculator class is responsible for computing the parking fee based on the selected ParkingSpot (or SpotType) and the duration of the parking session.

Since pricing is not fixed and can vary based on business rules such as hourly rates, weekend pricing, or peak-hour charges, the pricing logic should be flexible and extensible. To achieve this, we can use the Strategy Pattern, where different pricing algorithms are encapsulated into separate strategy classes.

This design allows the system to dynamically switch or combine pricing strategies without modifying existing code, thereby adhering to the Open/Closed Principle and improving maintainability.

public interface PricingStrategy {
    double apply(double currentPrice, Ticket ticket);
}

public class BasePricingStrategy implements PricingStrategy {

    @Override
    public double apply(double currentPrice, Ticket ticket) {
        SpotType type = ticket.getSpot().getType();

        double baseRate = type.getBaseRate(); // from enum
        long hours = Math.max(
                Duration.between(ticket.getEntryTime(), LocalDateTime.now()).toHours(),
                1
        );

        return baseRate * hours;
    }
}

public class WeekendPricingStrategy implements PricingStrategy {

    @Override
    public double apply(double currentPrice, Ticket ticket) {
        DayOfWeek day = LocalDateTime.now().getDayOfWeek();

        if (day == DayOfWeek.SATURDAY || day == DayOfWeek.SUNDAY) {
            return currentPrice * 1.5; // 50% surge
        }

        return currentPrice;
    }
}

public class PriceCalculator {

    private List<PricingStrategy> strategies;

    public PriceCalculator(List<PricingStrategy> strategies) {
        this.strategies = strategies;
    }

    public double calculate(Ticket ticket) {
        double price = 0;

        for (PricingStrategy strategy : strategies) {
            price = strategy.apply(price, ticket);
        }

        return price;
    }
}

Main

public class Main {

    public static void main(String[] args) {

        // Initialize SpotManager
        SpotManager spotManager = new SpotManager();
        spotManager.addSpot(new ParkingSpot("S1", SpotType.COMPACT));
        spotManager.addSpot(new ParkingSpot("S2", SpotType.REGULAR));
        spotManager.addSpot(new ParkingSpot("S3", SpotType.OVERSIZED));

        // Initialize TicketManager (in-memory)
        TicketManager ticketManager = new TicketManager();

        // Initialize Pricing Strategies
        List<PricingStrategy> strategies = List.of(
                new BasePricingStrategy(),
                new WeekendPricingStrategy(),
                new PeakHourPricingStrategy()
        );

        PriceCalculator priceCalculator = new PriceCalculator(strategies);

        // Simulate Vehicle Entry
        System.out.println("---- VEHICLE ENTRY ----");

        Car car = new Car("DL-123");
        ParkingSpot spot = spotManager.findSpot(car);

        if (spot == null) {
            System.out.println("No parking spot available!");
            return;
        }

        spot.assignVehicle(car);

        Ticket ticket = ticketManager.createTicket(car, spot);

        System.out.println("Vehicle parked. Ticket ID: " + ticket.getTicketId());

        // Simulate some time passing (for demo only)
        try {
            Thread.sleep(2000); // 2 seconds
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // Simulate Vehicle Exit
        System.out.println("\n---- VEHICLE EXIT ----");

        Ticket fetchedTicket = ticketManager.getTicket(ticket.getTicketId());

        double fee = priceCalculator.calculate(fetchedTicket);

        spotManager.freeSpot(fetchedTicket.getSpot());

        ticketManager.closeTicket(ticket.getTicketId(), fee);

        System.out.println("Parking fee: ₹" + fee);
        System.out.println("Vehicle exited successfully.");
    }
}


Final UML Diagram


final-uml-diagram