LLD Question 01

Parking Lot

Design a parking lot with floors, typed spots, vehicle allocation, ticketing, and fee calculation.

Learning note

Separate lot orchestration, spot matching, and fee calculation so each part can evolve independently.

Problem Statement

Design a parking lot system that can park different vehicle types across multiple floors and generate tickets for active parking sessions.

Functional Requirements

  • The parking lot can have multiple floors.
  • Each floor can have small, medium, and large spots.
  • Bikes, cars, and trucks should be parked only in compatible spots.
  • A ticket should be generated when a vehicle is parked.
  • A fee should be calculated when a vehicle exits.
  • The system should show current floor-level availability.

Core Entities

EntityResponsibility
ParkingLotMain orchestrator for entry, exit, floors, tickets, and fees.
ParkingFloorOwns spots and finds the first compatible available spot.
ParkingSpotBase abstraction for small, medium, and large spots.
VehicleBase abstraction for bike, car, and truck.
TicketStores vehicle, spot, floor, entry time, and exit time.
FeeCalculatorStrategy interface for pluggable fee calculation.

Design Decisions

Singleton for Parking Lot

The current design uses a singleton because one application instance represents one parking lot. It avoids accidental duplicate lot objects while the demo runs.

Strategy for Fee Calculation

ParkingLot depends on the FeeCalculator interface instead of hard-coding fee rules. This allows hourly, flat-rate, weekend, or surge pricing later without changing parking or exit flows.

Spot Compatibility

Spot matching is pushed into spot classes via canFitVehicle(vehicle). That keeps ParkingFloor simple: it only asks each spot whether it can fit the vehicle.

Main Flow

  1. Create a parking lot.
  2. Add one or more floors.
  3. For each vehicle, scan floors for an available compatible spot.
  4. Park the vehicle and generate a ticket.
  5. On exit, mark the ticket exit time, release the spot, calculate fee, and remove the active ticket.

Complete Code: Bottom-Up

Read this section from the small building blocks first and then move upward. The implementation ends at ParkingLot, where parking, tickets, floors, spots, vehicles, and fees converge, followed by Main as the demo entry point.

1. Vehicle.java

public abstract class Vehicle {
    protected String licensePlate;
    protected VehicleType vehicleType;
 
    public Vehicle(String licensePlate, VehicleType vehicleType){
        this.licensePlate = licensePlate;
        this.vehicleType = vehicleType;
    }
 
    public String getLicensePlate(){
        return licensePlate;
    }
 
    public VehicleType getVehicleType(){
        return vehicleType;
    }
}
 
enum VehicleType{
    BIKE, CAR, TRUCK
}

2. Bike.java

public class Bike extends Vehicle{
    public Bike(String licensePlate){
        super(licensePlate, VehicleType.BIKE);
    }
}

3. Car.java

public class Car extends Vehicle{
    public Car(String licensePlate){
        super(licensePlate, VehicleType.CAR);
    }
}

4. Truck.java

public class Truck extends Vehicle{
    public Truck(String licensePlate){
        super(licensePlate, VehicleType.TRUCK);
    }
}

5. ParkingSpot.java

abstract class ParkingSpot {
    private String spotId;
    private Vehicle occupiedVehicle;
    private SpotType type;
    private SpotStatus status;
 
    public ParkingSpot(String spotId, SpotType type){
        this.spotId = spotId;
        this.type = type;
        this.status = SpotStatus.AVAILABLE;
    }
 
    abstract boolean canFitVehicle(Vehicle vehicle);
 
    public boolean isSpotAvailable(){
        return status == SpotStatus.AVAILABLE;
    }
 
    public void parkVehicle(Vehicle vehicle){
        if(!isSpotAvailable())  throw new IllegalStateException("Spot is not Available!");
        this.occupiedVehicle = vehicle;
        this.status = SpotStatus.OCCUPIED;
    }
 
    public void removeVehicle(){
        this.occupiedVehicle = null;
        this.status = SpotStatus.AVAILABLE;
    }
 
    public String getSpotId() {
        return spotId;
    }
 
    public Vehicle getOccupiedVehicle() {
        return occupiedVehicle;
    }
 
    public SpotType getType() {
        return type;
    }
}
 
enum SpotType{
    SMALL, MEDIUM, LARGE
}
 
enum SpotStatus{
    OCCUPIED, AVAILABLE
}

6. SmallSpot.java

public class SmallSpot extends ParkingSpot{
    public SmallSpot(String spotId){
        super(spotId, SpotType.SMALL);
    }
 
    @Override
    boolean canFitVehicle(Vehicle vehicle){
        return vehicle.getVehicleType() == VehicleType.BIKE;
    }
}

7. MediumSpot.java

public class MediumSpot extends ParkingSpot{
    public MediumSpot(String spotId){
        super(spotId, SpotType.MEDIUM);
    }
 
    @Override
    boolean canFitVehicle(Vehicle vehicle){
        VehicleType type = vehicle.getVehicleType();
        return type == VehicleType.BIKE || type == VehicleType.CAR;
    }
}

8. LargeSpot.java

public class LargeSpot extends ParkingSpot{
    public LargeSpot(String spotId){
        super(spotId, SpotType.LARGE);
    }
 
    @Override
    boolean canFitVehicle(Vehicle vehicle){
        return true;
    }
}

9. Ticket.java

import java.time.Duration;
import java.time.LocalDateTime;
import java.util.UUID;
 
public class Ticket {
    private String ticketId;
    private Vehicle vehicle;
    private int floorNumber;
    private ParkingSpot spot;
    private LocalDateTime entryTime;
    private LocalDateTime exitTime;
 
    public Ticket(Vehicle vehicle, ParkingSpot spot, int floorNumber){
        this.ticketId = UUID.randomUUID().toString().substring(0,8).toUpperCase();
        this.vehicle = vehicle;
        this.spot = spot;
        this.floorNumber = floorNumber;
        this.entryTime = LocalDateTime.now();
    }
 
    public void markExit(){
        this.exitTime = LocalDateTime.now();
    }
 
    public long getDurationInHours(){
        LocalDateTime end = (exitTime != null) ? exitTime : LocalDateTime.now();
        long minutes = Duration.between(entryTime, end).toMinutes();
        return Math.max(1, (long) Math.ceil(minutes / 60.0));
    }
 
    public String getTicketId() {
        return ticketId;
    }
 
    public Vehicle getVehicle() {
        return vehicle;
    }
 
    public int getFloorNumber() {
        return floorNumber;
    }
 
    public ParkingSpot getSpot() {
        return spot;
    }
 
    public LocalDateTime getEntryTime() {
        return entryTime;
    }
 
    @Override
    public String toString(){
        return String.format("Ticket[%s] | Vehicle: %s | Floor: %d | Spot: %s | Entry: %s",
                ticketId, vehicle.getLicensePlate(), floorNumber, spot.getSpotId(), entryTime);
    }
}

10. FeeCalculator.java

public interface FeeCalculator {
    double calculateFee(Ticket ticket);
}

11. HourlyFeeCalculator.java

import java.util.Map;
 
public class HourlyFeeCalculator implements FeeCalculator{
    private static final Map<VehicleType, Double> RATES = Map.of(
            VehicleType.BIKE, 10.0,
            VehicleType.CAR, 20.0,
            VehicleType.TRUCK, 30.0
    );
 
    @Override
    public double calculateFee(Ticket ticket){
        Double rate = RATES.getOrDefault(ticket.getVehicle().getVehicleType(), 20.0);
        return rate * ticket.getDurationInHours();
    }
}

12. ParkingFloor.java

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
 
public class ParkingFloor {
    private int floorNum;
    private List<ParkingSpot> spots;
 
    public ParkingFloor(int floorNum, int small, int medium, int large){
        this.floorNum = floorNum;
        this.spots = new ArrayList<>();
 
        for(int i=0;i<small;i++){
            spots.add(new SmallSpot("F" + floorNum + "-S"+ i));
        }
        for(int i=0;i<medium;i++){
            spots.add(new MediumSpot("F" + floorNum + "-M"+ i));
        }
        for(int i=0;i<large;i++){
            spots.add(new LargeSpot("F" + floorNum + "-L"+ i));
        }
    }
 
    public Optional<ParkingSpot> findSpot(Vehicle vehicle){
        return spots.stream()
                .filter(spot -> spot.isSpotAvailable() && spot.canFitVehicle(vehicle))
                .findFirst();
    }
 
    public int getFloorNum(){
        return floorNum;
    }
 
    public long getAvailableSpotsCount(){
        return spots.stream().filter(ParkingSpot::isSpotAvailable).count();
    }
 
    public void printStatus(){
        System.out.println(" Floor " + floorNum + " | Available: " + getAvailableSpotsCount() + "/" + spots.size());
    }
}

13. ParkingLot.java

import java.util.*;
 
public class ParkingLot {
    private static volatile ParkingLot instance;
 
    private String name;
    private List<ParkingFloor> floors;
    private Map<String, Ticket> activeTickets;
    private FeeCalculator feeCalculator;
 
    private ParkingLot(String name){
        this.name = name;
        this.floors = new ArrayList<>();
        this.activeTickets = new HashMap<>();
        this.feeCalculator = new HourlyFeeCalculator();
    }
 
    public static ParkingLot getInstance(String name){
        if(instance == null){
            synchronized (ParkingLot.class){
                if(instance == null){
                    instance = new ParkingLot(name);
                }
            }
        }
 
        return instance;
    }
 
    public void addFloor(ParkingFloor floor){
        this.floors.add(floor);
    }
 
 
    // Entry Flow
 
    public Ticket parkVehicle(Vehicle vehicle){
        for(ParkingFloor floor: floors){
            Optional<ParkingSpot> spotOpt = floor.findSpot(vehicle);
            if(spotOpt.isPresent()){
                ParkingSpot spot = spotOpt.get();
                spot.parkVehicle(vehicle);
                Ticket ticket = new Ticket(vehicle, spot, floor.getFloorNum());
                activeTickets.put(ticket.getTicketId(), ticket);
                return ticket;
            }
        }
 
        throw new RuntimeException("Couldn't find a spot!");
    }
 
 
    // Exit Flow
    public double exitVehicle(String ticketId){
        Ticket ticket = activeTickets.get(ticketId);
        if(ticket == null)
            throw new IllegalStateException("Invalid Ticket!");
 
        ticket.markExit();
        ticket.getSpot().removeVehicle();
        activeTickets.remove(ticketId);
 
        double fee = feeCalculator.calculateFee(ticket);
        System.out.printf("Vehicle %s exited. Duration: %d hr(s). Fee: %.2f%n",
                ticket.getVehicle().getLicensePlate(), ticket.getDurationInHours(), fee);
        return fee;
    }
 
    public void displayStatus() {
        System.out.println("\n" + name + " - Status:");
        floors.forEach(ParkingFloor::printStatus);
        System.out.println("  Active tickets: " + activeTickets.size() + "\n");
    }
 
    public void setFeeCalculator(FeeCalculator calculator) {
        this.feeCalculator = calculator;
    }
 
}

14. Main.java

public class Main {
    public static void main(String[] args) {
        // Setup parking lot
        ParkingLot lot = ParkingLot.getInstance("HiTech City Parking");
        lot.addFloor(new ParkingFloor(1, 5, 10, 3));  // Floor 1: 5 small, 10 medium, 3 large
        lot.addFloor(new ParkingFloor(2, 5, 10, 3));  // Floor 2: same layout
 
        lot.displayStatus();
 
        // Park vehicles
        Vehicle bike1  = new Bike("TS-BIKE-001");
        Vehicle car1   = new Car("TS-CAR-042");
        Vehicle car2   = new Car("TS-CAR-099");
        Vehicle truck1 = new Truck("TS-TRUCK-007");
 
        Ticket t1 = lot.parkVehicle(bike1);
        Ticket t2 = lot.parkVehicle(car1);
        Ticket t3 = lot.parkVehicle(car2);
        Ticket t4 = lot.parkVehicle(truck1);
 
        lot.displayStatus();
 
        // Exit vehicles
        lot.exitVehicle(t2.getTicketId());
        lot.exitVehicle(t4.getTicketId());
 
        lot.displayStatus();
    }
}

What I Learned

  • Keep the orchestration logic in ParkingLot, not inside the entities.
  • Use abstractions for rules that may change, like pricing.
  • Spot selection can start simple with first-fit and later become a strategy.
  • Ticket lifecycle is important because it connects parking, exit, and billing.

Possible Improvements

  • Add a SpotAllocationStrategy for nearest-first or floor-priority allocation.
  • Add gates, display boards, and payment status.
  • Make singleton creation depend on a lot ID if multiple parking lots are needed later.
  • Add unit tests for compatibility, ticket duration, and fee calculation.