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
+ park(vehicle): Ticket
+ unpark(ticket): Payment
- spots: List<ParkingSpot>
+ find_available_spot(vehicle_type): ParkingSpot
- vehicle: Vehicle | None
- status: TicketStatus
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
ValetServiceclass that wrapsParkingLot.park(). - EV charging spots — Extend SpotType with
EV_CHARGING. Add aChargingSpotsubclass of ParkingSpot with charge rate and status. - Reserved spots — Add a
reserved_forfield on ParkingSpot. Skip reserved spots infind_available_spot()unless the vehicle matches. - Multiple entry/exit gates — Add
EntryGateandExitGateclasses. Each gate callsParkingLot.park()orunpark(). 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.