Episode 9 — System Design / 9.6 — LLD Problem Solving
9.6.a — Parking Lot System
Problem: Design a parking lot system that manages vehicles entering and exiting a multi-floor parking facility, assigns spots based on vehicle type, issues tickets, and calculates fees.
Table of Contents
- Requirements
- Entities and Enums
- Class Diagram
- Sequence Flow
- Complete Implementation
- Usage Walkthrough
- Edge Cases
- Key Takeaways
Requirements
Functional Requirements
- The parking lot has multiple floors, each floor has multiple spots.
- Spots come in different sizes: SMALL, MEDIUM, LARGE.
- Vehicles are of types: MOTORCYCLE, CAR, TRUCK.
- A motorcycle fits in SMALL+, a car fits in MEDIUM+, a truck needs LARGE.
- When a vehicle enters, the system finds the nearest available spot and issues a ticket.
- When a vehicle exits, the system calculates the fee based on duration.
- The system tracks how many spots are available per type per floor.
Non-Functional Requirements
- Thread-safe entry/exit (discussed conceptually; JS is single-threaded).
- Extensible pricing strategies.
- Multiple entry/exit points (future extension).
Out of Scope
- Payment gateway integration.
- Reservation system.
- Electric vehicle charging spots.
Entities and Enums
Enums
┌─────────────────────┐ ┌─────────────────────┐
│ VehicleType │ │ SpotType │
│─────────────────────│ │─────────────────────│
│ MOTORCYCLE │ │ SMALL │
│ CAR │ │ MEDIUM │
│ TRUCK │ │ LARGE │
└─────────────────────┘ └─────────────────────┘
┌─────────────────────┐
│ TicketStatus │
│─────────────────────│
│ ACTIVE │
│ PAID │
└─────────────────────┘
Vehicle-to-Spot Mapping
| Vehicle Type | Minimum Spot Required | Can Also Use |
|---|---|---|
| MOTORCYCLE | SMALL | MEDIUM, LARGE |
| CAR | MEDIUM | LARGE |
| TRUCK | LARGE | — |
Class Diagram
┌──────────────────────────────────────────────────────────────────────────┐
│ ParkingLot (Singleton) │
│──────────────────────────────────────────────────────────────────────────│
│ - name: string │
│ - floors: ParkingFloor[] │
│ - activeTickets: Map<string, Ticket> │
│ - pricingStrategy: PricingStrategy │
│──────────────────────────────────────────────────────────────────────────│
│ + enter(vehicle): Ticket │
│ + exit(ticketId): { fee, duration } │
│ + getAvailability(): object │
│ + setPricingStrategy(strategy): void │
└──────────────┬───────────────────────────────────────────────────────────┘
│ has-many
▼
┌──────────────────────────────────┐
│ ParkingFloor │
│──────────────────────────────────│
│ - floorNumber: number │
│ - spots: ParkingSpot[] │
│──────────────────────────────────│
│ + findAvailableSpot(type): Spot │
│ + getAvailableCount(type): number │
└──────────────┬───────────────────┘
│ has-many
▼
┌──────────────────────────────────┐ ┌──────────────────────────────┐
│ ParkingSpot │ │ Vehicle │
│──────────────────────────────────│ │──────────────────────────────│
│ - id: string │◄───────│ - licensePlate: string │
│ - floorNumber: number │parked │ - type: VehicleType │
│ - spotNumber: number │ at └──────────────────────────────┘
│ - type: SpotType │
│ - isOccupied: boolean │
│ - vehicle: Vehicle | null │
│──────────────────────────────────│
│ + park(vehicle): boolean │
│ + unpark(): Vehicle │
│ + canFit(vehicleType): boolean │
└──────────────────────────────────┘
┌──────────────────────────────────┐ ┌──────────────────────────────┐
│ Ticket │ │ PricingStrategy (iface) │
│──────────────────────────────────│ │──────────────────────────────│
│ - id: string │ │ + calculate(ticket): number │
│ - vehicle: Vehicle │ └──────────┬───────────────────┘
│ - spot: ParkingSpot │ │
│ - entryTime: Date │ ┌────────┴────────┐
│ - exitTime: Date | null │ ▼ ▼
│ - status: TicketStatus │ ┌──────────────┐ ┌──────────────┐
│──────────────────────────────────│ │ HourlyPricing │ │ FlatPricing │
│ + markPaid(exitTime): void │ └──────────────┘ └──────────────┘
└──────────────────────────────────┘
Sequence Flow
Entry Flow
Vehicle ParkingLot ParkingFloor ParkingSpot Ticket
│ │ │ │ │
│── enter() ───────►│ │ │ │
│ │── findSpot() ─────►│ │ │
│ │ │── canFit()? ─────►│ │
│ │ │◄── yes ───────────│ │
│ │◄── spot ───────────│ │ │
│ │── park(vehicle) ──────────────────────►│ │
│ │◄── success ───────────────────────────│ │
│ │── new Ticket() ──────────────────────────────────────────►│
│◄── ticket ────────│ │ │ │
Exit Flow
User ParkingLot PricingStrategy ParkingSpot Ticket
│ │ │ │ │
│── exit(id) ──────►│ │ │ │
│ │── lookup ticket ────────────────────────────────────────►│
│ │── calculate() ──►│ │ │
│ │◄── fee ──────────│ │ │
│ │── unpark() ─────────────────────────►│ │
│ │── markPaid() ────────────────────────────────────────────►│
│◄── { fee, dur } ──│ │ │ │
Complete Implementation
Enums
// ─── Enums ──────────────────────────────────────────────
const VehicleType = Object.freeze({
MOTORCYCLE: 'MOTORCYCLE',
CAR: 'CAR',
TRUCK: 'TRUCK',
});
const SpotType = Object.freeze({
SMALL: 'SMALL',
MEDIUM: 'MEDIUM',
LARGE: 'LARGE',
});
const TicketStatus = Object.freeze({
ACTIVE: 'ACTIVE',
PAID: 'PAID',
});
// Mapping: which spot types can a vehicle type use?
const VEHICLE_SPOT_COMPATIBILITY = Object.freeze({
[VehicleType.MOTORCYCLE]: [SpotType.SMALL, SpotType.MEDIUM, SpotType.LARGE],
[VehicleType.CAR]: [SpotType.MEDIUM, SpotType.LARGE],
[VehicleType.TRUCK]: [SpotType.LARGE],
});
Vehicle
// ─── Vehicle ────────────────────────────────────────────
class Vehicle {
constructor(licensePlate, type) {
this.licensePlate = licensePlate;
this.type = type; // VehicleType enum
}
}
ParkingSpot
// ─── ParkingSpot ────────────────────────────────────────
class ParkingSpot {
constructor(floorNumber, spotNumber, type) {
this.id = `F${floorNumber}-S${spotNumber}`;
this.floorNumber = floorNumber;
this.spotNumber = spotNumber;
this.type = type; // SpotType enum
this.isOccupied = false;
this.vehicle = null;
}
canFit(vehicleType) {
const compatibleSpots = VEHICLE_SPOT_COMPATIBILITY[vehicleType];
return !this.isOccupied && compatibleSpots.includes(this.type);
}
park(vehicle) {
if (!this.canFit(vehicle.type)) {
return false;
}
this.vehicle = vehicle;
this.isOccupied = true;
return true;
}
unpark() {
const vehicle = this.vehicle;
this.vehicle = null;
this.isOccupied = false;
return vehicle;
}
}
ParkingFloor
// ─── ParkingFloor ───────────────────────────────────────
class ParkingFloor {
constructor(floorNumber, spotConfig) {
this.floorNumber = floorNumber;
this.spots = [];
// spotConfig example: { SMALL: 5, MEDIUM: 10, LARGE: 3 }
let spotCounter = 1;
for (const [type, count] of Object.entries(spotConfig)) {
for (let i = 0; i < count; i++) {
this.spots.push(new ParkingSpot(floorNumber, spotCounter++, type));
}
}
}
findAvailableSpot(vehicleType) {
// Returns the first available compatible spot (nearest)
return this.spots.find(spot => spot.canFit(vehicleType)) || null;
}
getAvailableCount(spotType) {
return this.spots.filter(s => s.type === spotType && !s.isOccupied).length;
}
getTotalCount(spotType) {
return this.spots.filter(s => s.type === spotType).length;
}
}
Ticket
// ─── Ticket ─────────────────────────────────────────────
class Ticket {
static _counter = 0;
constructor(vehicle, spot) {
this.id = `T-${++Ticket._counter}`;
this.vehicle = vehicle;
this.spot = spot;
this.entryTime = new Date();
this.exitTime = null;
this.status = TicketStatus.ACTIVE;
}
markPaid(exitTime) {
this.exitTime = exitTime;
this.status = TicketStatus.PAID;
}
getDurationHours() {
const exit = this.exitTime || new Date();
return (exit - this.entryTime) / (1000 * 60 * 60);
}
}
Pricing Strategies (Strategy Pattern)
// ─── Pricing Strategies ─────────────────────────────────
class HourlyPricingStrategy {
constructor(rates) {
// rates example: { MOTORCYCLE: 10, CAR: 20, TRUCK: 40 }
this.rates = rates;
}
calculate(ticket) {
const hours = Math.ceil(ticket.getDurationHours());
const rate = this.rates[ticket.vehicle.type] || 0;
return hours * rate;
}
}
class FlatPricingStrategy {
constructor(rates) {
// Flat rate regardless of duration
this.rates = rates;
}
calculate(ticket) {
return this.rates[ticket.vehicle.type] || 0;
}
}
ParkingLot (Singleton)
// ─── ParkingLot (Singleton) ─────────────────────────────
class ParkingLot {
static _instance = null;
constructor(name) {
if (ParkingLot._instance) {
return ParkingLot._instance;
}
this.name = name;
this.floors = [];
this.activeTickets = new Map(); // ticketId -> Ticket
this.vehicleTickets = new Map(); // licensePlate -> Ticket
this.pricingStrategy = new HourlyPricingStrategy({
[VehicleType.MOTORCYCLE]: 10,
[VehicleType.CAR]: 20,
[VehicleType.TRUCK]: 40,
});
ParkingLot._instance = this;
}
static getInstance(name) {
if (!ParkingLot._instance) {
new ParkingLot(name);
}
return ParkingLot._instance;
}
static resetInstance() {
ParkingLot._instance = null;
}
// ── Configuration ─────────────────────────────────────
addFloor(spotConfig) {
const floorNumber = this.floors.length + 1;
this.floors.push(new ParkingFloor(floorNumber, spotConfig));
}
setPricingStrategy(strategy) {
this.pricingStrategy = strategy;
}
// ── Entry ─────────────────────────────────────────────
enter(vehicle) {
// Check if vehicle is already parked
if (this.vehicleTickets.has(vehicle.licensePlate)) {
throw new Error(`Vehicle ${vehicle.licensePlate} is already parked.`);
}
// Search floors top to bottom for the nearest available spot
for (const floor of this.floors) {
const spot = floor.findAvailableSpot(vehicle.type);
if (spot) {
spot.park(vehicle);
const ticket = new Ticket(vehicle, spot);
this.activeTickets.set(ticket.id, ticket);
this.vehicleTickets.set(vehicle.licensePlate, ticket);
console.log(
`[ENTRY] ${vehicle.type} ${vehicle.licensePlate} -> Spot ${spot.id} | Ticket: ${ticket.id}`
);
return ticket;
}
}
throw new Error(`No available spot for vehicle type: ${vehicle.type}`);
}
// ── Exit ──────────────────────────────────────────────
exit(ticketId) {
const ticket = this.activeTickets.get(ticketId);
if (!ticket) {
throw new Error(`Ticket ${ticketId} not found or already paid.`);
}
const exitTime = new Date();
const fee = this.pricingStrategy.calculate(ticket);
// Free the spot
ticket.spot.unpark();
// Mark ticket as paid
ticket.markPaid(exitTime);
// Remove from active maps
this.activeTickets.delete(ticketId);
this.vehicleTickets.delete(ticket.vehicle.licensePlate);
const durationHrs = ticket.getDurationHours().toFixed(2);
console.log(
`[EXIT] ${ticket.vehicle.licensePlate} | Duration: ${durationHrs}h | Fee: $${fee}`
);
return { ticket, fee, durationHours: parseFloat(durationHrs) };
}
// ── Availability ──────────────────────────────────────
getAvailability() {
const report = {};
for (const floor of this.floors) {
report[`Floor ${floor.floorNumber}`] = {
SMALL: `${floor.getAvailableCount(SpotType.SMALL)}/${floor.getTotalCount(SpotType.SMALL)}`,
MEDIUM: `${floor.getAvailableCount(SpotType.MEDIUM)}/${floor.getTotalCount(SpotType.MEDIUM)}`,
LARGE: `${floor.getAvailableCount(SpotType.LARGE)}/${floor.getTotalCount(SpotType.LARGE)}`,
};
}
return report;
}
isFull(vehicleType) {
return this.floors.every(floor => !floor.findAvailableSpot(vehicleType));
}
}
Usage Walkthrough
// ─── Demo ───────────────────────────────────────────────
// Reset for fresh start
ParkingLot.resetInstance();
// Create parking lot
const lot = ParkingLot.getInstance('City Center Parking');
// Add 2 floors
lot.addFloor({ SMALL: 5, MEDIUM: 10, LARGE: 2 });
lot.addFloor({ SMALL: 3, MEDIUM: 8, LARGE: 3 });
// Check availability
console.log('Initial availability:', lot.getAvailability());
// Floor 1: { SMALL: "5/5", MEDIUM: "10/10", LARGE: "2/2" }
// Floor 2: { SMALL: "3/3", MEDIUM: "8/8", LARGE: "3/3" }
// Vehicles enter
const bike = new Vehicle('BIKE-001', VehicleType.MOTORCYCLE);
const car1 = new Vehicle('CAR-001', VehicleType.CAR);
const car2 = new Vehicle('CAR-002', VehicleType.CAR);
const truck = new Vehicle('TRUCK-001', VehicleType.TRUCK);
const t1 = lot.enter(bike); // -> F1-S1 (first SMALL spot)
const t2 = lot.enter(car1); // -> F1-S6 (first MEDIUM spot)
const t3 = lot.enter(car2); // -> F1-S7 (second MEDIUM spot)
const t4 = lot.enter(truck); // -> F1-S16 (first LARGE spot)
console.log('After entries:', lot.getAvailability());
// Vehicle exits
const result = lot.exit(t2.id);
// [EXIT] CAR-001 | Duration: 0.00h | Fee: $20
console.log('Fee charged:', result.fee);
// Switch to flat pricing
lot.setPricingStrategy(new FlatPricingStrategy({
[VehicleType.MOTORCYCLE]: 50,
[VehicleType.CAR]: 100,
[VehicleType.TRUCK]: 200,
}));
const result2 = lot.exit(t4.id);
// [EXIT] TRUCK-001 | Duration: 0.00h | Fee: $200
Edge Cases
| Edge Case | How It Is Handled |
|---|---|
| Lot is full for a vehicle type | enter() throws an error |
| Same vehicle tries to enter twice | enter() checks vehicleTickets map and throws |
| Invalid ticket ID on exit | exit() throws "not found" error |
| Motorcycle parks in MEDIUM spot | Allowed via VEHICLE_SPOT_COMPATIBILITY when SMALL is full |
| Zero-duration parking | Math.ceil ensures minimum 1 hour charge (hourly strategy) |
| Pricing strategy changes mid-park | Fee is calculated at exit time with the current strategy |
Key Takeaways
- Singleton Pattern is natural for a parking lot — there is exactly one instance.
- Strategy Pattern for pricing makes it easy to add new billing models (per-minute, weekend rates, subscription) without modifying the
ParkingLotclass. - Enums + compatibility map cleanly encode business rules about which vehicle fits where.
- Separation of concerns:
ParkingSpothandles parking logic,ParkingFloorhandles searching,ParkingLotorchestrates the flow. - In interviews, always draw the entry/exit sequence diagram — it shows you understand the full lifecycle.
Next -> 9.6.b — Vending Machine