Design a Parking Lot

intermediate 2-4 YOE lld parking-lot design-question

This is THE most asked LLD interview question. If we’re preparing for only one design question, this should be it. We’re designing a multi-floor parking lot that handles different vehicle types, assigns spots, issues tickets, and calculates fees.

The beauty of this question is that it touches almost every OOP concept — inheritance, enums, composition, and multiple design patterns. Let’s build it piece by piece.

Requirements

Functional:

  • Multiple floors, each with multiple parking spots
  • Three vehicle types: Car, Bike, Truck
  • Three spot sizes: Small (bikes), Medium (cars), Large (trucks)
  • Entry generates a ticket with timestamp
  • Exit calculates fee based on duration
  • Track available spots per floor and per type

Assumptions:

  • One vehicle per spot (trucks don’t take 2 spots — keeps it simple)
  • Hourly pricing, different rates per vehicle type
  • Single parking lot instance (Singleton)

Key Classes & Relationships

Parking Lot Class Diagram
ParkingLot (Singleton)
- floors: List<Floor>
+ park(vehicle): Ticket
+ unpark(ticket): Payment
│ has many
Floor
- floor_number: int
- spots: List<ParkingSpot>
+ find_available_spot(vehicle_type): ParkingSpot
│ has many
ParkingSpot
- spot_type: SpotType
- vehicle: Vehicle | None
Ticket
- vehicle, spot, entry_time
- status: TicketStatus
Vehicle
- license, type: VehicleType
Car / Bike / Truck

Core Implementation

from abc import ABC, abstractmethod
from enum import Enum
from datetime import datetime
import uuid

# ---- Enums ----
class VehicleType(Enum):
    BIKE = "BIKE"
    CAR = "CAR"
    TRUCK = "TRUCK"

class SpotType(Enum):
    SMALL = "SMALL"
    MEDIUM = "MEDIUM"
    LARGE = "LARGE"

class TicketStatus(Enum):
    ACTIVE = "ACTIVE"
    PAID = "PAID"

# ---- Vehicles ----
class Vehicle:
    def __init__(self, license_plate: str, vehicle_type: VehicleType):
        self.license_plate = license_plate
        self.vehicle_type = vehicle_type

class Car(Vehicle):
    def __init__(self, license_plate: str):
        super().__init__(license_plate, VehicleType.CAR)

class Bike(Vehicle):
    def __init__(self, license_plate: str):
        super().__init__(license_plate, VehicleType.BIKE)

class Truck(Vehicle):
    def __init__(self, license_plate: str):
        super().__init__(license_plate, VehicleType.TRUCK)

# ---- Mapping: which vehicle goes in which spot ----
VEHICLE_TO_SPOT = {
    VehicleType.BIKE: SpotType.SMALL,
    VehicleType.CAR: SpotType.MEDIUM,
    VehicleType.TRUCK: SpotType.LARGE,
}

# ---- Parking Spot ----
class ParkingSpot:
    def __init__(self, spot_id: str, spot_type: SpotType):
        self.spot_id = spot_id
        self.spot_type = spot_type
        self.vehicle = None

    def is_available(self) -> bool:
        return self.vehicle is None

    def park(self, vehicle: Vehicle):
        self.vehicle = vehicle

    def unpark(self):
        self.vehicle = None

# ---- Floor ----
class Floor:
    def __init__(self, floor_number: int, spots: list):
        self.floor_number = floor_number
        self.spots = spots  # list of ParkingSpot

    def find_available_spot(self, vehicle_type: VehicleType):
        needed = VEHICLE_TO_SPOT[vehicle_type]
        for spot in self.spots:
            if spot.spot_type == needed and spot.is_available():
                return spot
        return None

# ---- Ticket ----
class Ticket:
    def __init__(self, vehicle: Vehicle, spot: ParkingSpot):
        self.ticket_id = str(uuid.uuid4())[:8]
        self.vehicle = vehicle
        self.spot = spot
        self.entry_time = datetime.now()
        self.status = TicketStatus.ACTIVE

# ---- Pricing Strategy ----
class PricingStrategy(ABC):
    @abstractmethod
    def calculate(self, hours: float, vehicle_type: VehicleType) -> float:
        pass

class HourlyPricing(PricingStrategy):
    RATES = {
        VehicleType.BIKE: 10,
        VehicleType.CAR: 20,
        VehicleType.TRUCK: 40,
    }

    def calculate(self, hours: float, vehicle_type: VehicleType) -> float:
        rate = self.RATES[vehicle_type]
        return max(1, int(hours + 0.99)) * rate  # round up to next hour

# ---- Payment ----
class Payment:
    def __init__(self, ticket: Ticket, amount: float):
        self.ticket = ticket
        self.amount = amount
        self.paid_at = datetime.now()

# ---- Parking Lot (Singleton) ----
class ParkingLot:
    _instance = None

    def __new__(cls, *args, **kwargs):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

    def __init__(self, floors: list = None, pricing: PricingStrategy = None):
        if not hasattr(self, '_initialized'):
            self.floors = floors or []
            self.pricing = pricing or HourlyPricing()
            self._tickets = {}  # ticket_id -> Ticket
            self._initialized = True

    def park(self, vehicle: Vehicle) -> Ticket:
        for floor in self.floors:
            spot = floor.find_available_spot(vehicle.vehicle_type)
            if spot:
                spot.park(vehicle)
                ticket = Ticket(vehicle, spot)
                self._tickets[ticket.ticket_id] = ticket
                print(f"Parked {vehicle.license_plate} at spot {spot.spot_id}")
                return ticket
        print("No available spot!")
        return None

    def unpark(self, ticket_id: str) -> Payment:
        ticket = self._tickets.get(ticket_id)
        if not ticket or ticket.status != TicketStatus.ACTIVE:
            print("Invalid ticket!")
            return None

        hours = (datetime.now() - ticket.entry_time).total_seconds() / 3600
        amount = self.pricing.calculate(hours, ticket.vehicle.vehicle_type)

        ticket.spot.unpark()
        ticket.status = TicketStatus.PAID
        payment = Payment(ticket, amount)
        print(f"Vehicle {ticket.vehicle.license_plate} -- Fee: ${amount}")
        return payment

# ---- Usage ----
floors = [
    Floor(1, [ParkingSpot(f"1-S{i}", SpotType.SMALL) for i in range(5)]
            + [ParkingSpot(f"1-M{i}", SpotType.MEDIUM) for i in range(10)]
            + [ParkingSpot(f"1-L{i}", SpotType.LARGE) for i in range(3)]),
    Floor(2, [ParkingSpot(f"2-M{i}", SpotType.MEDIUM) for i in range(10)]),
]

lot = ParkingLot(floors)
ticket = lot.park(Car("KA-01-1234"))   # Parked KA-01-1234 at spot 1-M0
lot.unpark(ticket.ticket_id)            # Vehicle KA-01-1234 -- Fee: $20
// ---- Enums ----
const VehicleType = Object.freeze({ BIKE: "BIKE", CAR: "CAR", TRUCK: "TRUCK" });
const SpotType = Object.freeze({ SMALL: "SMALL", MEDIUM: "MEDIUM", LARGE: "LARGE" });
const TicketStatus = Object.freeze({ ACTIVE: "ACTIVE", PAID: "PAID" });

const VEHICLE_TO_SPOT = {
  [VehicleType.BIKE]: SpotType.SMALL,
  [VehicleType.CAR]: SpotType.MEDIUM,
  [VehicleType.TRUCK]: SpotType.LARGE,
};

// ---- Vehicles ----
class Vehicle {
  constructor(licensePlate, vehicleType) {
    this.licensePlate = licensePlate;
    this.vehicleType = vehicleType;
  }
}
class Car extends Vehicle { constructor(lp) { super(lp, VehicleType.CAR); } }
class Bike extends Vehicle { constructor(lp) { super(lp, VehicleType.BIKE); } }
class Truck extends Vehicle { constructor(lp) { super(lp, VehicleType.TRUCK); } }

// ---- Parking Spot ----
class ParkingSpot {
  constructor(spotId, spotType) {
    this.spotId = spotId;
    this.spotType = spotType;
    this.vehicle = null;
  }
  isAvailable() { return this.vehicle === null; }
  park(vehicle) { this.vehicle = vehicle; }
  unpark() { this.vehicle = null; }
}

// ---- Floor ----
class Floor {
  constructor(floorNumber, spots) {
    this.floorNumber = floorNumber;
    this.spots = spots;
  }
  findAvailableSpot(vehicleType) {
    const needed = VEHICLE_TO_SPOT[vehicleType];
    return this.spots.find(s => s.spotType === needed && s.isAvailable()) || null;
  }
}

// ---- Ticket ----
class Ticket {
  constructor(vehicle, spot) {
    this.ticketId = Math.random().toString(36).slice(2, 10);
    this.vehicle = vehicle;
    this.spot = spot;
    this.entryTime = new Date();
    this.status = TicketStatus.ACTIVE;
  }
}

// ---- Pricing Strategy ----
class HourlyPricing {
  static RATES = { [VehicleType.BIKE]: 10, [VehicleType.CAR]: 20, [VehicleType.TRUCK]: 40 };
  calculate(hours, vehicleType) {
    return Math.max(1, Math.ceil(hours)) * HourlyPricing.RATES[vehicleType];
  }
}

// ---- Payment ----
class Payment {
  constructor(ticket, amount) {
    this.ticket = ticket;
    this.amount = amount;
    this.paidAt = new Date();
  }
}

// ---- Parking Lot (Singleton) ----
class ParkingLot {
  static #instance = null;
  static getInstance(floors, pricing) {
    if (!ParkingLot.#instance) {
      ParkingLot.#instance = new ParkingLot(floors, pricing);
    }
    return ParkingLot.#instance;
  }

  constructor(floors = [], pricing = new HourlyPricing()) {
    this.floors = floors;
    this.pricing = pricing;
    this.tickets = new Map();
  }

  park(vehicle) {
    for (const floor of this.floors) {
      const spot = floor.findAvailableSpot(vehicle.vehicleType);
      if (spot) {
        spot.park(vehicle);
        const ticket = new Ticket(vehicle, spot);
        this.tickets.set(ticket.ticketId, ticket);
        console.log(`Parked ${vehicle.licensePlate} at ${spot.spotId}`);
        return ticket;
      }
    }
    console.log("No available spot!");
    return null;
  }

  unpark(ticketId) {
    const ticket = this.tickets.get(ticketId);
    if (!ticket || ticket.status !== TicketStatus.ACTIVE) return null;

    const hours = (Date.now() - ticket.entryTime.getTime()) / 3600000;
    const amount = this.pricing.calculate(hours, ticket.vehicle.vehicleType);

    ticket.spot.unpark();
    ticket.status = TicketStatus.PAID;
    console.log(`Vehicle ${ticket.vehicle.licensePlate} -- Fee: $${amount}`);
    return new Payment(ticket, amount);
  }
}

// Usage
const floors = [
  new Floor(1, [
    ...Array.from({ length: 5 }, (_, i) => new ParkingSpot(`1-S${i}`, SpotType.SMALL)),
    ...Array.from({ length: 10 }, (_, i) => new ParkingSpot(`1-M${i}`, SpotType.MEDIUM)),
    ...Array.from({ length: 3 }, (_, i) => new ParkingSpot(`1-L${i}`, SpotType.LARGE)),
  ]),
];
const lot = ParkingLot.getInstance(floors);
const ticket = lot.park(new Car("KA-01-1234"));
lot.unpark(ticket.ticketId);
import java.util.*;
import java.time.*;

enum VehicleType { BIKE, CAR, TRUCK }
enum SpotType { SMALL, MEDIUM, LARGE }
enum TicketStatus { ACTIVE, PAID }

// ---- Vehicles ----
abstract class Vehicle {
    String licensePlate;
    VehicleType type;
    Vehicle(String lp, VehicleType t) { this.licensePlate = lp; this.type = t; }
}
class Car extends Vehicle { Car(String lp) { super(lp, VehicleType.CAR); } }
class Bike extends Vehicle { Bike(String lp) { super(lp, VehicleType.BIKE); } }
class Truck extends Vehicle { Truck(String lp) { super(lp, VehicleType.TRUCK); } }

// ---- Parking Spot ----
class ParkingSpot {
    String spotId;
    SpotType spotType;
    Vehicle vehicle;

    ParkingSpot(String id, SpotType type) { this.spotId = id; this.spotType = type; }
    boolean isAvailable() { return vehicle == null; }
    void park(Vehicle v) { this.vehicle = v; }
    void unpark() { this.vehicle = null; }
}

// ---- Floor ----
class Floor {
    int floorNumber;
    List<ParkingSpot> spots;

    Floor(int num, List<ParkingSpot> spots) { this.floorNumber = num; this.spots = spots; }

    ParkingSpot findAvailableSpot(VehicleType vType) {
        SpotType needed = switch (vType) {
            case BIKE -> SpotType.SMALL;
            case CAR -> SpotType.MEDIUM;
            case TRUCK -> SpotType.LARGE;
        };
        return spots.stream()
            .filter(s -> s.spotType == needed && s.isAvailable())
            .findFirst().orElse(null);
    }
}

// ---- Ticket ----
class Ticket {
    String ticketId = UUID.randomUUID().toString().substring(0, 8);
    Vehicle vehicle;
    ParkingSpot spot;
    LocalDateTime entryTime = LocalDateTime.now();
    TicketStatus status = TicketStatus.ACTIVE;

    Ticket(Vehicle v, ParkingSpot s) { this.vehicle = v; this.spot = s; }
}

// ---- Pricing Strategy ----
interface PricingStrategy {
    double calculate(double hours, VehicleType type);
}

class HourlyPricing implements PricingStrategy {
    private static final Map<VehicleType, Integer> RATES = Map.of(
        VehicleType.BIKE, 10, VehicleType.CAR, 20, VehicleType.TRUCK, 40
    );

    public double calculate(double hours, VehicleType type) {
        return Math.max(1, (int) Math.ceil(hours)) * RATES.get(type);
    }
}

// ---- Parking Lot (Singleton) ----
class ParkingLot {
    private static ParkingLot instance;
    private List<Floor> floors;
    private PricingStrategy pricing;
    private Map<String, Ticket> tickets = new HashMap<>();

    private ParkingLot(List<Floor> floors, PricingStrategy pricing) {
        this.floors = floors;
        this.pricing = pricing;
    }

    public static ParkingLot getInstance(List<Floor> floors, PricingStrategy pricing) {
        if (instance == null) instance = new ParkingLot(floors, pricing);
        return instance;
    }

    public Ticket park(Vehicle vehicle) {
        for (Floor floor : floors) {
            ParkingSpot spot = floor.findAvailableSpot(vehicle.type);
            if (spot != null) {
                spot.park(vehicle);
                Ticket ticket = new Ticket(vehicle, spot);
                tickets.put(ticket.ticketId, ticket);
                System.out.println("Parked " + vehicle.licensePlate + " at " + spot.spotId);
                return ticket;
            }
        }
        System.out.println("No available spot!");
        return null;
    }

    public double unpark(String ticketId) {
        Ticket ticket = tickets.get(ticketId);
        if (ticket == null || ticket.status != TicketStatus.ACTIVE) return -1;

        double hours = Duration.between(ticket.entryTime, LocalDateTime.now()).toMinutes() / 60.0;
        double amount = pricing.calculate(hours, ticket.vehicle.type);

        ticket.spot.unpark();
        ticket.status = TicketStatus.PAID;
        System.out.printf("Vehicle %s -- Fee: $%.0f%n", ticket.vehicle.licensePlate, amount);
        return amount;
    }
}

Design Patterns Used

Singleton — ParkingLot is a single instance. There’s only one parking lot, so we don’t want multiple objects floating around with conflicting state.

Strategy — PricingStrategy lets us swap pricing logic. Maybe weekends have surge pricing, or we offer flat-rate night parking. New pricing? Just add a new strategy class. No changes to ParkingLot.

Composition — ParkingLot has Floors, Floors have ParkingSpots. Clean hierarchy. No inheritance abuse.

Extensions

These are common follow-up questions interviewers love to throw:

  • Valet parking — Add a VehicleType-aware assignment where attendants get a queue of vehicles to park. We can add a ValetService class that wraps ParkingLot.park().
  • EV charging spots — Extend SpotType with EV_CHARGING. Add a ChargingSpot subclass of ParkingSpot with charge rate and status.
  • Reserved spots — Add a reserved_for field on ParkingSpot. Skip reserved spots in find_available_spot() unless the vehicle matches.
  • Multiple entry/exit gates — Add EntryGate and ExitGate classes. Each gate calls ParkingLot.park() or unpark(). Use thread-safe methods if concurrent access is needed.
  • Dynamic pricing — Time-of-day pricing, weekend surge, loyalty discounts. All handled by adding new PricingStrategy implementations.

In simple language, this design breaks the parking lot into small, focused classes. Each class does one thing. Spots know if they’re free. Floors know how to find spots. The lot ties it all together. Pricing is pluggable. That’s exactly what interviewers want to see.