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

  1. Requirements
  2. Entities and Enums
  3. Class Diagram
  4. Sequence Flow
  5. Complete Implementation
  6. Usage Walkthrough
  7. Edge Cases
  8. Key Takeaways

Requirements

Functional Requirements

  1. The parking lot has multiple floors, each floor has multiple spots.
  2. Spots come in different sizes: SMALL, MEDIUM, LARGE.
  3. Vehicles are of types: MOTORCYCLE, CAR, TRUCK.
  4. A motorcycle fits in SMALL+, a car fits in MEDIUM+, a truck needs LARGE.
  5. When a vehicle enters, the system finds the nearest available spot and issues a ticket.
  6. When a vehicle exits, the system calculates the fee based on duration.
  7. 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 TypeMinimum Spot RequiredCan Also Use
MOTORCYCLESMALLMEDIUM, LARGE
CARMEDIUMLARGE
TRUCKLARGE

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 CaseHow It Is Handled
Lot is full for a vehicle typeenter() throws an error
Same vehicle tries to enter twiceenter() checks vehicleTickets map and throws
Invalid ticket ID on exitexit() throws "not found" error
Motorcycle parks in MEDIUM spotAllowed via VEHICLE_SPOT_COMPATIBILITY when SMALL is full
Zero-duration parkingMath.ceil ensures minimum 1 hour charge (hourly strategy)
Pricing strategy changes mid-parkFee is calculated at exit time with the current strategy

Key Takeaways

  1. Singleton Pattern is natural for a parking lot — there is exactly one instance.
  2. Strategy Pattern for pricing makes it easy to add new billing models (per-minute, weekend rates, subscription) without modifying the ParkingLot class.
  3. Enums + compatibility map cleanly encode business rules about which vehicle fits where.
  4. Separation of concerns: ParkingSpot handles parking logic, ParkingFloor handles searching, ParkingLot orchestrates the flow.
  5. In interviews, always draw the entry/exit sequence diagram — it shows you understand the full lifecycle.

Next -> 9.6.b — Vending Machine