Designing a Parking Lot System

Difficulty: Beginner Patterns: Strategy, Factory, Observer, Composition Asked at: Flipkart, PhonePe, Amazon, Google, Uber


Functional Requirements

  1. Parking lot has multiple floors, each floor has spots of sizes: Small, Medium, Large
  2. Vehicle types: Motorcycle, Car, Truck
  3. Motorcycle fits in any spot. Car fits in Medium/Large. Truck needs Large only.
  4. On entry: assign nearest available matching spot, issue ticket
  5. On exit: calculate fee based on duration and vehicle type, process payment
  6. Strategy-based pricing - hourly, flat-rate, or weekend pricing (swappable)

Non-Functional Requirements

  1. Thread-safety - two vehicles shouldn’t be assigned the same spot concurrently
  2. O(1) spot lookup - use appropriate data structures for fast spot assignment
  3. Extensibility - adding new vehicle types, spot types, or pricing = minimal code changes

Core Entities

Entity Description
Vehicle License plate + type (Motorcycle, Car, Truck)
VehicleType Enum: MOTORCYCLE, CAR, TRUCK
Spot Has a size, tracks if occupied, knows which vehicle is parked
SpotSize Enum: SMALL, MEDIUM, LARGE
Floor Collection of spots, can find available spot for a vehicle
ParkingLot Multiple floors, manages park/unpark operations
Ticket Issued on entry - links vehicle, spot, entry time
PricingStrategy Interface for fee calculation (Strategy pattern)
HourlyPricing Charges per hour based on vehicle type
FlatRatePricing Flat fee regardless of duration
Payment Result of fee calculation

Class Diagram

classDiagram
    class VehicleType {
        <<enumeration>>
        MOTORCYCLE
        CAR
        TRUCK
    }

    class SpotSize {
        <<enumeration>>
        SMALL
        MEDIUM
        LARGE
    }

    class Vehicle {
        -String licensePlate
        -VehicleType type
    }

    class Spot {
        -int id
        -int floorNumber
        -SpotSize size
        -boolean occupied
        -Vehicle currentVehicle
        +canFit(Vehicle) boolean
        +assign(Vehicle)
        +free()
    }

    class Floor {
        -int floorNumber
        -List~Spot~ spots
        -Map~SpotSize, Queue~Spot~~ availableSpots
        +getAvailableSpot(Vehicle) Spot
        +freeSpot(Spot)
    }

    class Ticket {
        -String id
        -Vehicle vehicle
        -Spot spot
        -LocalDateTime entryTime
    }

    class PricingStrategy {
        <<interface>>
        +calculateFee(Ticket, LocalDateTime exitTime) double
    }

    class HourlyPricing {
        +calculateFee(Ticket, LocalDateTime) double
    }

    class FlatRatePricing {
        +calculateFee(Ticket, LocalDateTime) double
    }

    class Payment {
        -Ticket ticket
        -double amount
        -LocalDateTime paidAt
    }

    class ParkingLot {
        -List~Floor~ floors
        -Map~String, Ticket~ activeTickets
        -PricingStrategy pricingStrategy
        -ReentrantLock lock
        +parkVehicle(Vehicle) Ticket
        +unparkVehicle(String ticketId) Payment
        +setPricingStrategy(PricingStrategy)
        +getAvailableSpotCount() int
    }

    ParkingLot --> Floor
    ParkingLot --> PricingStrategy
    ParkingLot --> Ticket
    Floor --> Spot
    Spot --> Vehicle
    Ticket --> Vehicle
    Ticket --> Spot
    PricingStrategy <|.. HourlyPricing
    PricingStrategy <|.. FlatRatePricing

Design Patterns

Pattern Where Why
Strategy PricingStrategy interface with HourlyPricing / FlatRatePricing Swap pricing at runtime. Weekend pricing = one new class, zero changes to ParkingLot.
Factory SpotFactory creates spots of different sizes Decouple spot creation from floor initialization logic.
Composition ParkingLot HAS Floors, Floors HAVE Spots Flexible hierarchical structure over inheritance.
Observer (extension) Display panel notified on spot status change Decouple UI from core logic.

Data Structures

Component Structure Why
Available spots per size Map<SpotSize, Queue<Spot>> O(1) dequeue to get next available spot
Active tickets HashMap<String, Ticket> O(1) lookup on exit by ticket ID
Floors ArrayList<Floor> Sequential floor-by-floor search
Spot ID mapping Implicit via floor + spot index No extra map needed

How It All Fits Together

Here’s what happens when a car arrives at the lot:

  1. Driver enters → system calls parkVehicle(car)
  2. ParkingLot acquires a lock (thread-safety for concurrent arrivals)
  3. Checks if car is already parked (duplicate prevention via vehicleTickets map)
  4. Iterates through floors, asking each for an available MEDIUM or LARGE spot
  5. Floor checks its Queue<Spot> for the smallest fitting size first (best-fit strategy)
  6. Spot is assigned, ticket is issued with entry timestamp
  7. Lock is released, ticket returned to driver

When the car leaves:

  1. Driver presents ticket → system calls unparkVehicle(ticketId)
  2. ParkingLot acquires lock, looks up ticket in O(1) from activeTickets map
  3. PricingStrategy calculates fee based on duration and vehicle type
  4. Spot is freed and returned to the floor’s available queue
  5. Vehicle tracking removed, payment receipt generated and returned

Complete Code

VehicleType.java

These two enums define the type vocabulary for the entire system. Every sizing/pricing decision branches on these values, so centralizing them as enums prevents stringly-typed bugs.

package parkinglot.model;

public enum VehicleType {
    MOTORCYCLE,
    CAR,
    TRUCK
}
from enum import Enum

class VehicleType(Enum):
    MOTORCYCLE = "MOTORCYCLE"
    CAR = "CAR"
    TRUCK = "TRUCK"
#pragma once

enum class VehicleType {
    MOTORCYCLE,
    CAR,
    TRUCK
};
const VehicleType = Object.freeze({
    MOTORCYCLE: 'MOTORCYCLE',
    CAR: 'CAR',
    TRUCK: 'TRUCK'
});

SpotSize.java

package parkinglot.model;

public enum SpotSize {

    SMALL,
    MEDIUM,
    LARGE
}
from enum import Enum

class SpotSize(Enum):
    SMALL = "SMALL"
    MEDIUM = "MEDIUM"
    LARGE = "LARGE"
#pragma once

enum class SpotSize {
    SMALL,
    MEDIUM,
    LARGE
};
const SpotSize = Object.freeze({
    SMALL: 'SMALL',
    MEDIUM: 'MEDIUM',
    LARGE: 'LARGE'
});

Vehicle.java

A vehicle is the “thing being parked.” It’s an immutable value object identified by its license plate. Equality is based on licensePlate so we can use it as a HashMap key for duplicate-parking detection.

package parkinglot.model;

public class Vehicle {
    private final String licensePlate;
    private final VehicleType type;

    public Vehicle(String licensePlate, VehicleType type) {
        this.licensePlate = licensePlate;
        this.type = type;
    }

    public String getLicensePlate() { return licensePlate; }
    public VehicleType getType() { return type; }

    @Override
    public String toString() {
        return type + " [" + licensePlate + "]";
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Vehicle v = (Vehicle) o;
        return licensePlate.equals(v.licensePlate);
    }

    @Override
    public int hashCode() {
        return licensePlate.hashCode();
    }
}
class Vehicle:
    def __init__(self, license_plate: str, vehicle_type: VehicleType):
        self._license_plate = license_plate
        self._type = vehicle_type

    @property
    def license_plate(self) -> str:
        return self._license_plate

    @property
    def type(self) -> VehicleType:
        return self._type

    def __str__(self) -> str:
        return f"{self._type.value} [{self._license_plate}]"

    def __eq__(self, other) -> bool:
        if not isinstance(other, Vehicle):
            return False
        return self._license_plate == other._license_plate

    def __hash__(self) -> int:
        return hash(self._license_plate)
#pragma once
#include <string>
#include "VehicleType.hpp"

class Vehicle {
private:
    std::string licensePlate;
    VehicleType type;

public:
    Vehicle(std::string licensePlate, VehicleType type)
        : licensePlate(std::move(licensePlate)), type(type) {}

    const std::string& getLicensePlate() const { return licensePlate; }
    VehicleType getType() const { return type; }

    bool operator==(const Vehicle& other) const {
        return licensePlate == other.licensePlate;
    }

    std::string toString() const {
        return vehicleTypeToString(type) + " [" + licensePlate + "]";
    }
};

// Hash specialization for use in unordered_map
namespace std {
    template<>
    struct hash<Vehicle> {
        size_t operator()(const Vehicle& v) const {
            return hash<string>()(v.getLicensePlate());
        }
    };
}
class Vehicle {
    #licensePlate;
    #type;

    constructor(licensePlate, type) {
        this.#licensePlate = licensePlate;
        this.#type = type;
    }

    get licensePlate() { return this.#licensePlate; }
    get type() { return this.#type; }

    toString() {
        return `${this.#type} [${this.#licensePlate}]`;
    }

    equals(other) {
        if (!(other instanceof Vehicle)) return false;
        return this.#licensePlate === other.licensePlate;
    }
}

Spot.java

A spot is the atomic unit of the parking lot - it knows its size, whether it’s occupied, and which vehicle is in it. The canFit() method encodes the sizing rules (motorcycle → any, car → medium/large, truck → large only) so the Floor doesn’t need to know vehicle-specific logic.

package parkinglot.model;

public class Spot {
    private final int id;
    private final int floorNumber;
    private final SpotSize size;
    private boolean occupied;
    private Vehicle currentVehicle;

    public Spot(int id, int floorNumber, SpotSize size) {
        this.id = id;
        this.floorNumber = floorNumber;
        this.size = size;
        this.occupied = false;
        this.currentVehicle = null;
    }

    /**
     * Check if this spot can fit the given vehicle.
     * Rules:
     *   Motorcycle → any spot (SMALL, MEDIUM, LARGE)
     *   Car → MEDIUM or LARGE only
     *   Truck → LARGE only
     */
    public boolean canFit(Vehicle vehicle) {
        if (occupied) return false;

        switch (vehicle.getType()) {
            case MOTORCYCLE:
                return true; // fits anywhere
            case CAR:
                return size == SpotSize.MEDIUM || size == SpotSize.LARGE;
            case TRUCK:
                return size == SpotSize.LARGE;
            default:
                return false;
        }
    }

    public void assign(Vehicle vehicle) {
        if (occupied) {
            throw new IllegalStateException("Spot " + id + " is already occupied");
        }
        this.occupied = true;
        this.currentVehicle = vehicle;
    }

    public void free() {
        this.occupied = false;
        this.currentVehicle = null;
    }

    public int getId() { return id; }
    public int getFloorNumber() { return floorNumber; }
    public SpotSize getSize() { return size; }
    public boolean isOccupied() { return occupied; }
    public Vehicle getCurrentVehicle() { return currentVehicle; }

    @Override
    public String toString() {
        return "Floor " + floorNumber + " | Spot " + id + " (" + size + ")" +
               (occupied ? " [OCCUPIED by " + currentVehicle + "]" : " [AVAILABLE]");
    }
}
class Spot:
    def __init__(self, spot_id: int, floor_number: int, size: SpotSize):
        self._id = spot_id
        self._floor_number = floor_number
        self._size = size
        self._occupied = False
        self._current_vehicle: Vehicle | None = None

    def can_fit(self, vehicle: Vehicle) -> bool:
        """
        Check if this spot can fit the given vehicle.
        Rules:
          Motorcycle -> any spot (SMALL, MEDIUM, LARGE)
          Car -> MEDIUM or LARGE only
          Truck -> LARGE only
        """
        if self._occupied:
            return False

        match vehicle.type:
            case VehicleType.MOTORCYCLE:
                return True  # fits anywhere
            case VehicleType.CAR:
                return self._size in (SpotSize.MEDIUM, SpotSize.LARGE)
            case VehicleType.TRUCK:
                return self._size == SpotSize.LARGE
            case _:
                return False

    def assign(self, vehicle: Vehicle) -> None:
        if self._occupied:
            raise RuntimeError(f"Spot {self._id} is already occupied")
        self._occupied = True
        self._current_vehicle = vehicle

    def free(self) -> None:
        self._occupied = False
        self._current_vehicle = None

    @property
    def id(self) -> int:
        return self._id

    @property
    def floor_number(self) -> int:
        return self._floor_number

    @property
    def size(self) -> SpotSize:
        return self._size

    @property
    def is_occupied(self) -> bool:
        return self._occupied

    @property
    def current_vehicle(self) -> Vehicle | None:
        return self._current_vehicle

    def __str__(self) -> str:
        status = (f" [OCCUPIED by {self._current_vehicle}]"
                  if self._occupied else " [AVAILABLE]")
        return f"Floor {self._floor_number} | Spot {self._id} ({self._size.value}){status}"
#pragma once
#include <string>
#include <stdexcept>
#include <optional>
#include "SpotSize.hpp"
#include "Vehicle.hpp"

class Spot {
private:
    int id;
    int floorNumber;
    SpotSize size;
    bool occupied;
    std::optional<Vehicle> currentVehicle;

public:
    Spot(int id, int floorNumber, SpotSize size)
        : id(id), floorNumber(floorNumber), size(size),
          occupied(false), currentVehicle(std::nullopt) {}

    /**
     * Check if this spot can fit the given vehicle.
     * Rules:
     *   Motorcycle -> any spot (SMALL, MEDIUM, LARGE)
     *   Car -> MEDIUM or LARGE only
     *   Truck -> LARGE only
     */
    bool canFit(const Vehicle& vehicle) const {
        if (occupied) return false;

        switch (vehicle.getType()) {
            case VehicleType::MOTORCYCLE:
                return true; // fits anywhere
            case VehicleType::CAR:
                return size == SpotSize::MEDIUM || size == SpotSize::LARGE;
            case VehicleType::TRUCK:
                return size == SpotSize::LARGE;
            default:
                return false;
        }
    }

    void assign(const Vehicle& vehicle) {
        if (occupied) {
            throw std::runtime_error("Spot " + std::to_string(id) + " is already occupied");
        }
        occupied = true;
        currentVehicle = vehicle;
    }

    void free() {
        occupied = false;
        currentVehicle = std::nullopt;
    }

    int getId() const { return id; }
    int getFloorNumber() const { return floorNumber; }
    SpotSize getSize() const { return size; }
    bool isOccupied() const { return occupied; }
    const std::optional<Vehicle>& getCurrentVehicle() const { return currentVehicle; }

    std::string toString() const {
        std::string status = occupied
            ? " [OCCUPIED by " + currentVehicle->toString() + "]"
            : " [AVAILABLE]";
        return "Floor " + std::to_string(floorNumber) + " | Spot " +
               std::to_string(id) + " (" + spotSizeToString(size) + ")" + status;
    }
};
class Spot {
    #id;
    #floorNumber;
    #size;
    #occupied;
    #currentVehicle;

    constructor(id, floorNumber, size) {
        this.#id = id;
        this.#floorNumber = floorNumber;
        this.#size = size;
        this.#occupied = false;
        this.#currentVehicle = null;
    }

    /**
     * Check if this spot can fit the given vehicle.
     * Rules:
     *   Motorcycle -> any spot (SMALL, MEDIUM, LARGE)
     *   Car -> MEDIUM or LARGE only
     *   Truck -> LARGE only
     */
    canFit(vehicle) {
        if (this.#occupied) return false;

        switch (vehicle.type) {
            case VehicleType.MOTORCYCLE:
                return true; // fits anywhere
            case VehicleType.CAR:
                return this.#size === SpotSize.MEDIUM || this.#size === SpotSize.LARGE;
            case VehicleType.TRUCK:
                return this.#size === SpotSize.LARGE;
            default:
                return false;
        }
    }

    assign(vehicle) {
        if (this.#occupied) {
            throw new Error(`Spot ${this.#id} is already occupied`);
        }
        this.#occupied = true;
        this.#currentVehicle = vehicle;
    }

    free() {
        this.#occupied = false;
        this.#currentVehicle = null;
    }

    get id() { return this.#id; }
    get floorNumber() { return this.#floorNumber; }
    get size() { return this.#size; }
    get isOccupied() { return this.#occupied; }
    get currentVehicle() { return this.#currentVehicle; }

    toString() {
        const status = this.#occupied
            ? ` [OCCUPIED by ${this.#currentVehicle}]`
            : ' [AVAILABLE]';
        return `Floor ${this.#floorNumber} | Spot ${this.#id} (${this.#size})${status}`;
    }
}

Floor.java

A floor owns a collection of spots and manages availability. The key data structure choice here is Map<SpotSize, Queue<Spot>> - a queue per spot size gives us O(1) retrieval of the next available spot instead of scanning all spots linearly. When a vehicle arrives, we try the smallest fitting size first (best-fit) so motorcycles don’t waste large spots.

package parkinglot.model;

import java.util.*;

public class Floor {
    private final int floorNumber;
    private final List<Spot> spots;
    private final Map<SpotSize, Queue<Spot>> availableSpots;

    public Floor(int floorNumber, int smallCount, int mediumCount, int largeCount) {
        this.floorNumber = floorNumber;
        this.spots = new ArrayList<>();
        this.availableSpots = new EnumMap<>(SpotSize.class);

        // Initialize queues for each size
        availableSpots.put(SpotSize.SMALL, new LinkedList<>());
        availableSpots.put(SpotSize.MEDIUM, new LinkedList<>());
        availableSpots.put(SpotSize.LARGE, new LinkedList<>());

        int id = 1;
        // Create spots and add to available queues
        for (int i = 0; i < smallCount; i++) {
            Spot spot = new Spot(id++, floorNumber, SpotSize.SMALL);
            spots.add(spot);
            availableSpots.get(SpotSize.SMALL).offer(spot);
        }
        for (int i = 0; i < mediumCount; i++) {
            Spot spot = new Spot(id++, floorNumber, SpotSize.MEDIUM);
            spots.add(spot);
            availableSpots.get(SpotSize.MEDIUM).offer(spot);
        }
        for (int i = 0; i < largeCount; i++) {
            Spot spot = new Spot(id++, floorNumber, SpotSize.LARGE);
            spots.add(spot);
            availableSpots.get(SpotSize.LARGE).offer(spot);
        }
    }

    /**
     * Find and assign an available spot for the vehicle.
     * Uses the smallest fitting spot first (best fit).
     * Returns null if no spot available on this floor.
     */
    public Spot getAvailableSpot(Vehicle vehicle) {
        // Try spots in order: smallest fitting first
        List<SpotSize> candidates = getFittingSizes(vehicle.getType());

        for (SpotSize size : candidates) {
            Queue<Spot> queue = availableSpots.get(size);
            if (!queue.isEmpty()) {
                return queue.poll(); // remove from available
            }
        }
        return null; // no spot on this floor
    }

    /**
     * Return a spot back to the available pool.
     */
    public void freeSpot(Spot spot) {
        availableSpots.get(spot.getSize()).offer(spot);
    }

    /**
     * Get compatible spot sizes for a vehicle type (smallest first).
     */
    private List<SpotSize> getFittingSizes(VehicleType type) {
        switch (type) {
            case MOTORCYCLE:
                return Arrays.asList(SpotSize.SMALL, SpotSize.MEDIUM, SpotSize.LARGE);
            case CAR:
                return Arrays.asList(SpotSize.MEDIUM, SpotSize.LARGE);
            case TRUCK:
                return Collections.singletonList(SpotSize.LARGE);
            default:
                return Collections.emptyList();
        }
    }

    public int getFloorNumber() { return floorNumber; }
    public List<Spot> getSpots() { return Collections.unmodifiableList(spots); }

    public int getAvailableCount() {
        return availableSpots.values().stream()
                .mapToInt(Queue::size)
                .sum();
    }

    public int getTotalCount() {
        return spots.size();
    }
}
from collections import deque

class Floor:
    def __init__(self, floor_number: int, small_count: int, medium_count: int, large_count: int):
        self._floor_number = floor_number
        self._spots: list[Spot] = []
        self._available_spots: dict[SpotSize, deque[Spot]] = {
            SpotSize.SMALL: deque(),
            SpotSize.MEDIUM: deque(),
            SpotSize.LARGE: deque(),
        }

        spot_id = 1
        # Create spots and add to available queues
        for _ in range(small_count):
            spot = Spot(spot_id, floor_number, SpotSize.SMALL)
            self._spots.append(spot)
            self._available_spots[SpotSize.SMALL].append(spot)
            spot_id += 1
        for _ in range(medium_count):
            spot = Spot(spot_id, floor_number, SpotSize.MEDIUM)
            self._spots.append(spot)
            self._available_spots[SpotSize.MEDIUM].append(spot)
            spot_id += 1
        for _ in range(large_count):
            spot = Spot(spot_id, floor_number, SpotSize.LARGE)
            self._spots.append(spot)
            self._available_spots[SpotSize.LARGE].append(spot)
            spot_id += 1

    def get_available_spot(self, vehicle: Vehicle) -> Spot | None:
        """
        Find and assign an available spot for the vehicle.
        Uses the smallest fitting spot first (best fit).
        Returns None if no spot available on this floor.
        """
        candidates = self._get_fitting_sizes(vehicle.type)

        for size in candidates:
            queue = self._available_spots[size]
            if queue:
                return queue.popleft()  # remove from available
        return None

    def free_spot(self, spot: Spot) -> None:
        """Return a spot back to the available pool."""
        self._available_spots[spot.size].append(spot)

    def _get_fitting_sizes(self, vehicle_type: VehicleType) -> list[SpotSize]:
        """Get compatible spot sizes for a vehicle type (smallest first)."""
        match vehicle_type:
            case VehicleType.MOTORCYCLE:
                return [SpotSize.SMALL, SpotSize.MEDIUM, SpotSize.LARGE]
            case VehicleType.CAR:
                return [SpotSize.MEDIUM, SpotSize.LARGE]
            case VehicleType.TRUCK:
                return [SpotSize.LARGE]
            case _:
                return []

    @property
    def floor_number(self) -> int:
        return self._floor_number

    @property
    def spots(self) -> list[Spot]:
        return list(self._spots)

    @property
    def available_count(self) -> int:
        return sum(len(q) for q in self._available_spots.values())

    @property
    def total_count(self) -> int:
        return len(self._spots)
#pragma once
#include <vector>
#include <queue>
#include <unordered_map>
#include "Spot.hpp"

class Floor {
private:
    int floorNumber;
    std::vector<Spot> spots;
    std::unordered_map<int, std::queue<Spot*>> availableSpots; // SpotSize enum as int key

    std::vector<SpotSize> getFittingSizes(VehicleType type) const {
        switch (type) {
            case VehicleType::MOTORCYCLE:
                return {SpotSize::SMALL, SpotSize::MEDIUM, SpotSize::LARGE};
            case VehicleType::CAR:
                return {SpotSize::MEDIUM, SpotSize::LARGE};
            case VehicleType::TRUCK:
                return {SpotSize::LARGE};
            default:
                return {};
        }
    }

public:
    Floor(int floorNumber, int smallCount, int mediumCount, int largeCount)
        : floorNumber(floorNumber) {

        // Initialize queues for each size
        availableSpots[static_cast<int>(SpotSize::SMALL)] = std::queue<Spot*>();
        availableSpots[static_cast<int>(SpotSize::MEDIUM)] = std::queue<Spot*>();
        availableSpots[static_cast<int>(SpotSize::LARGE)] = std::queue<Spot*>();

        int id = 1;
        for (int i = 0; i < smallCount; ++i) {
            spots.emplace_back(id++, floorNumber, SpotSize::SMALL);
        }
        for (int i = 0; i < mediumCount; ++i) {
            spots.emplace_back(id++, floorNumber, SpotSize::MEDIUM);
        }
        for (int i = 0; i < largeCount; ++i) {
            spots.emplace_back(id++, floorNumber, SpotSize::LARGE);
        }

        // Add pointers to available queues (after vector is fully built)
        for (auto& spot : spots) {
            availableSpots[static_cast<int>(spot.getSize())].push(&spot);
        }
    }

    /**
     * Find and assign an available spot for the vehicle.
     * Uses the smallest fitting spot first (best fit).
     * Returns nullptr if no spot available on this floor.
     */
    Spot* getAvailableSpot(const Vehicle& vehicle) {
        auto candidates = getFittingSizes(vehicle.getType());

        for (SpotSize size : candidates) {
            auto& queue = availableSpots[static_cast<int>(size)];
            if (!queue.empty()) {
                Spot* spot = queue.front();
                queue.pop();
                return spot;
            }
        }
        return nullptr;
    }

    void freeSpot(Spot* spot) {
        availableSpots[static_cast<int>(spot->getSize())].push(spot);
    }

    int getFloorNumber() const { return floorNumber; }
    const std::vector<Spot>& getSpots() const { return spots; }

    int getAvailableCount() const {
        int count = 0;
        for (const auto& [key, queue] : availableSpots) {
            count += queue.size();
        }
        return count;
    }

    int getTotalCount() const { return static_cast<int>(spots.size()); }
};
class Floor {
    #floorNumber;
    #spots;
    #availableSpots;

    constructor(floorNumber, smallCount, mediumCount, largeCount) {
        this.#floorNumber = floorNumber;
        this.#spots = [];
        this.#availableSpots = {
            [SpotSize.SMALL]: [],
            [SpotSize.MEDIUM]: [],
            [SpotSize.LARGE]: [],
        };

        let id = 1;
        // Create spots and add to available queues
        for (let i = 0; i < smallCount; i++) {
            const spot = new Spot(id++, floorNumber, SpotSize.SMALL);
            this.#spots.push(spot);
            this.#availableSpots[SpotSize.SMALL].push(spot);
        }
        for (let i = 0; i < mediumCount; i++) {
            const spot = new Spot(id++, floorNumber, SpotSize.MEDIUM);
            this.#spots.push(spot);
            this.#availableSpots[SpotSize.MEDIUM].push(spot);
        }
        for (let i = 0; i < largeCount; i++) {
            const spot = new Spot(id++, floorNumber, SpotSize.LARGE);
            this.#spots.push(spot);
            this.#availableSpots[SpotSize.LARGE].push(spot);
        }
    }

    /**
     * Find and assign an available spot for the vehicle.
     * Uses the smallest fitting spot first (best fit).
     * Returns null if no spot available on this floor.
     */
    getAvailableSpot(vehicle) {
        const candidates = this.#getFittingSizes(vehicle.type);

        for (const size of candidates) {
            const queue = this.#availableSpots[size];
            if (queue.length > 0) {
                return queue.shift(); // remove from available
            }
        }
        return null;
    }

    freeSpot(spot) {
        this.#availableSpots[spot.size].push(spot);
    }

    #getFittingSizes(vehicleType) {
        switch (vehicleType) {
            case VehicleType.MOTORCYCLE:
                return [SpotSize.SMALL, SpotSize.MEDIUM, SpotSize.LARGE];
            case VehicleType.CAR:
                return [SpotSize.MEDIUM, SpotSize.LARGE];
            case VehicleType.TRUCK:
                return [SpotSize.LARGE];
            default:
                return [];
        }
    }

    get floorNumber() { return this.#floorNumber; }
    get spots() { return [...this.#spots]; }

    get availableCount() {
        return Object.values(this.#availableSpots)
            .reduce((sum, queue) => sum + queue.length, 0);
    }

    get totalCount() { return this.#spots.length; }
}

Ticket.java

A ticket is the proof of parking - it captures which vehicle is in which spot, and when they entered. The UUID-based ID ensures uniqueness without a central counter. This is the link between entry and exit: the driver presents the ticket ID at departure.

package parkinglot.model;

import java.time.LocalDateTime;
import java.util.UUID;

public class Ticket {
    private final String id;
    private final Vehicle vehicle;
    private final Spot spot;
    private final LocalDateTime entryTime;

    public Ticket(Vehicle vehicle, Spot spot, LocalDateTime entryTime) {
        this.id = UUID.randomUUID().toString().substring(0, 8).toUpperCase();
        this.vehicle = vehicle;
        this.spot = spot;
        this.entryTime = entryTime;
    }

    public String getId() { return id; }
    public Vehicle getVehicle() { return vehicle; }
    public Spot getSpot() { return spot; }
    public LocalDateTime getEntryTime() { return entryTime; }

    @Override
    public String toString() {
        return "Ticket[" + id + "] " + vehicle + " → " + spot +
               " | Entry: " + entryTime;
    }
}
import uuid
from datetime import datetime

class Ticket:
    def __init__(self, vehicle: Vehicle, spot: Spot, entry_time: datetime):
        self._id = uuid.uuid4().hex[:8].upper()
        self._vehicle = vehicle
        self._spot = spot
        self._entry_time = entry_time

    @property
    def id(self) -> str:
        return self._id

    @property
    def vehicle(self) -> Vehicle:
        return self._vehicle

    @property
    def spot(self) -> Spot:
        return self._spot

    @property
    def entry_time(self) -> datetime:
        return self._entry_time

    def __str__(self) -> str:
        return (f"Ticket[{self._id}] {self._vehicle} -> {self._spot} "
                f"| Entry: {self._entry_time}")
#pragma once
#include <string>
#include <chrono>
#include <random>
#include <sstream>
#include <iomanip>
#include "Vehicle.hpp"
#include "Spot.hpp"

using TimePoint = std::chrono::system_clock::time_point;

class Ticket {
private:
    std::string id;
    Vehicle vehicle;
    Spot* spot;
    TimePoint entryTime;

    static std::string generateId() {
        static std::mt19937 rng(std::random_device{}());
        std::uniform_int_distribution<int> dist(0, 15);
        const char* hex = "0123456789ABCDEF";
        std::string result;
        for (int i = 0; i < 8; ++i) {
            result += hex[dist(rng)];
        }
        return result;
    }

public:
    Ticket(Vehicle vehicle, Spot* spot, TimePoint entryTime)
        : id(generateId()), vehicle(std::move(vehicle)),
          spot(spot), entryTime(entryTime) {}

    const std::string& getId() const { return id; }
    const Vehicle& getVehicle() const { return vehicle; }
    Spot* getSpot() const { return spot; }
    TimePoint getEntryTime() const { return entryTime; }

    std::string toString() const {
        return "Ticket[" + id + "] " + vehicle.toString() + " -> " + spot->toString();
    }
};
class Ticket {
    #id;
    #vehicle;
    #spot;
    #entryTime;

    constructor(vehicle, spot, entryTime) {
        this.#id = crypto.randomUUID().substring(0, 8).toUpperCase();
        this.#vehicle = vehicle;
        this.#spot = spot;
        this.#entryTime = entryTime;
    }

    get id() { return this.#id; }
    get vehicle() { return this.#vehicle; }
    get spot() { return this.#spot; }
    get entryTime() { return this.#entryTime; }

    toString() {
        return `Ticket[${this.#id}] ${this.#vehicle} -> ${this.#spot} | Entry: ${this.#entryTime.toISOString()}`;
    }
}

Payment.java

Payment is the output of the unpark flow - it bundles the fee, hours parked, and timestamp into a receipt. Separating it from the pricing logic keeps the “what to charge” decision (strategy) independent from the “record what was charged” concern (this class).

package parkinglot.model;

import java.time.LocalDateTime;

public class Payment {
    private final Ticket ticket;
    private final double amount;
    private final long hoursParked;
    private final LocalDateTime paidAt;

    public Payment(Ticket ticket, double amount, long hoursParked) {
        this.ticket = ticket;
        this.amount = amount;
        this.hoursParked = hoursParked;
        this.paidAt = LocalDateTime.now();
    }

    public Ticket getTicket() { return ticket; }
    public double getAmount() { return amount; }
    public long getHoursParked() { return hoursParked; }
    public LocalDateTime getPaidAt() { return paidAt; }

    @Override
    public String toString() {
        return "Payment: ₹" + amount + " | " + hoursParked + " hrs | " +
               ticket.getVehicle() + " | Ticket: " + ticket.getId();
    }
}
from datetime import datetime

class Payment:
    def __init__(self, ticket: Ticket, amount: float, hours_parked: int):
        self._ticket = ticket
        self._amount = amount
        self._hours_parked = hours_parked
        self._paid_at = datetime.now()

    @property
    def ticket(self) -> Ticket:
        return self._ticket

    @property
    def amount(self) -> float:
        return self._amount

    @property
    def hours_parked(self) -> int:
        return self._hours_parked

    @property
    def paid_at(self) -> datetime:
        return self._paid_at

    def __str__(self) -> str:
        return (f"Payment: ₹{self._amount} | {self._hours_parked} hrs | "
                f"{self._ticket.vehicle} | Ticket: {self._ticket.id}")
#pragma once
#include <string>
#include <chrono>
#include "Ticket.hpp"

class Payment {
private:
    Ticket ticket;
    double amount;
    long hoursParked;
    TimePoint paidAt;

public:
    Payment(Ticket ticket, double amount, long hoursParked)
        : ticket(std::move(ticket)), amount(amount),
          hoursParked(hoursParked), paidAt(std::chrono::system_clock::now()) {}

    const Ticket& getTicket() const { return ticket; }
    double getAmount() const { return amount; }
    long getHoursParked() const { return hoursParked; }
    TimePoint getPaidAt() const { return paidAt; }

    std::string toString() const {
        return "Payment: Rs" + std::to_string(amount) + " | " +
               std::to_string(hoursParked) + " hrs | " +
               ticket.getVehicle().toString() + " | Ticket: " + ticket.getId();
    }
};
class Payment {
    #ticket;
    #amount;
    #hoursParked;
    #paidAt;

    constructor(ticket, amount, hoursParked) {
        this.#ticket = ticket;
        this.#amount = amount;
        this.#hoursParked = hoursParked;
        this.#paidAt = new Date();
    }

    get ticket() { return this.#ticket; }
    get amount() { return this.#amount; }
    get hoursParked() { return this.#hoursParked; }
    get paidAt() { return this.#paidAt; }

    toString() {
        return `Payment: ₹${this.#amount} | ${this.#hoursParked} hrs | ${this.#ticket.vehicle} | Ticket: ${this.#ticket.id}`;
    }
}

PricingStrategy.java (Strategy Interface)

This is the heart of the extensibility story.

💡 Strategy pattern = define a family of algorithms, encapsulate each one, and make them interchangeable at runtime. Adding a new pricing model = one new class, zero changes to existing code.

The interface takes a ticket and exit time, returns a fee. ParkingLot delegates all pricing decisions here - it never contains pricing logic itself.

package parkinglot.pricing;

import parkinglot.model.Ticket;
import java.time.LocalDateTime;

public interface PricingStrategy {
    /**
     * Calculate the parking fee for a ticket.
     * @param ticket The parking ticket
     * @param exitTime The time of exit
     * @return Fee amount in ₹
     */
    double calculateFee(Ticket ticket, LocalDateTime exitTime);
}
from abc import ABC, abstractmethod
from datetime import datetime

class PricingStrategy(ABC):
    """Interface for fee calculation (Strategy pattern)."""

    @abstractmethod
    def calculate_fee(self, ticket: Ticket, exit_time: datetime) -> float:
        """
        Calculate the parking fee for a ticket.
        :param ticket: The parking ticket
        :param exit_time: The time of exit
        :return: Fee amount in ₹
        """
        pass
#pragma once
#include "Ticket.hpp"

class PricingStrategy {
public:
    virtual ~PricingStrategy() = default;

    /**
     * Calculate the parking fee for a ticket.
     * @param ticket The parking ticket
     * @param exitTime The time of exit
     * @return Fee amount in ₹
     */
    virtual double calculateFee(const Ticket& ticket, TimePoint exitTime) const = 0;
};
class PricingStrategy {
    /**
     * Calculate the parking fee for a ticket.
     * @param {Ticket} ticket - The parking ticket
     * @param {Date} exitTime - The time of exit
     * @returns {number} Fee amount in ₹
     */
    calculateFee(ticket, exitTime) {
        throw new Error('calculateFee() must be implemented by subclass');
    }
}

HourlyPricing.java

The default strategy - charges per hour with different rates per vehicle type. Uses ceiling division ((minutes + 59) / 60) so even 1 minute counts as a full hour. The EnumMap gives us O(1) rate lookup by vehicle type.

package parkinglot.pricing;

import parkinglot.model.Ticket;
import parkinglot.model.VehicleType;

import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.EnumMap;
import java.util.Map;

/**
 * Charges per hour based on vehicle type.
 * Minimum charge = 1 hour (even if parked for 5 minutes).
 */
public class HourlyPricing implements PricingStrategy {
    private final Map<VehicleType, Double> rates;

    public HourlyPricing() {
        rates = new EnumMap<>(VehicleType.class);
        rates.put(VehicleType.MOTORCYCLE, 10.0);
        rates.put(VehicleType.CAR, 20.0);
        rates.put(VehicleType.TRUCK, 30.0);
    }

    public HourlyPricing(double motorcycleRate, double carRate, double truckRate) {
        rates = new EnumMap<>(VehicleType.class);
        rates.put(VehicleType.MOTORCYCLE, motorcycleRate);
        rates.put(VehicleType.CAR, carRate);
        rates.put(VehicleType.TRUCK, truckRate);
    }

    @Override
    public double calculateFee(Ticket ticket, LocalDateTime exitTime) {
        long minutes = ChronoUnit.MINUTES.between(ticket.getEntryTime(), exitTime);
        long hours = (minutes + 59) / 60; // round up to next hour
        if (hours == 0) hours = 1; // minimum 1 hour

        double rate = rates.getOrDefault(ticket.getVehicle().getType(), 20.0);
        return hours * rate;
    }
}
import math
from datetime import datetime

class HourlyPricing(PricingStrategy):
    """
    Charges per hour based on vehicle type.
    Minimum charge = 1 hour (even if parked for 5 minutes).
    """

    def __init__(self, motorcycle_rate: float = 10.0,
                 car_rate: float = 20.0, truck_rate: float = 30.0):
        self._rates = {
            VehicleType.MOTORCYCLE: motorcycle_rate,
            VehicleType.CAR: car_rate,
            VehicleType.TRUCK: truck_rate,
        }

    def calculate_fee(self, ticket: Ticket, exit_time: datetime) -> float:
        minutes = (exit_time - ticket.entry_time).total_seconds() / 60
        hours = math.ceil(minutes / 60)
        if hours == 0:
            hours = 1  # minimum 1 hour

        rate = self._rates.get(ticket.vehicle.type, 20.0)
        return hours * rate
#pragma once
#include <unordered_map>
#include "PricingStrategy.hpp"

/**
 * Charges per hour based on vehicle type.
 * Minimum charge = 1 hour (even if parked for 5 minutes).
 */
class HourlyPricing : public PricingStrategy {
private:
    std::unordered_map<int, double> rates; // VehicleType as int key

public:
    HourlyPricing() {
        rates[static_cast<int>(VehicleType::MOTORCYCLE)] = 10.0;
        rates[static_cast<int>(VehicleType::CAR)] = 20.0;
        rates[static_cast<int>(VehicleType::TRUCK)] = 30.0;
    }

    HourlyPricing(double motorcycleRate, double carRate, double truckRate) {
        rates[static_cast<int>(VehicleType::MOTORCYCLE)] = motorcycleRate;
        rates[static_cast<int>(VehicleType::CAR)] = carRate;
        rates[static_cast<int>(VehicleType::TRUCK)] = truckRate;
    }

    double calculateFee(const Ticket& ticket, TimePoint exitTime) const override {
        auto duration = std::chrono::duration_cast<std::chrono::minutes>(
            exitTime - ticket.getEntryTime());
        long minutes = duration.count();
        long hours = (minutes + 59) / 60; // round up to next hour
        if (hours == 0) hours = 1; // minimum 1 hour

        int key = static_cast<int>(ticket.getVehicle().getType());
        double rate = rates.count(key) ? rates.at(key) : 20.0;
        return hours * rate;
    }
};
/**
 * Charges per hour based on vehicle type.
 * Minimum charge = 1 hour (even if parked for 5 minutes).
 */
class HourlyPricing extends PricingStrategy {
    #rates;

    constructor(motorcycleRate = 10, carRate = 20, truckRate = 30) {
        super();
        this.#rates = {
            [VehicleType.MOTORCYCLE]: motorcycleRate,
            [VehicleType.CAR]: carRate,
            [VehicleType.TRUCK]: truckRate,
        };
    }

    calculateFee(ticket, exitTime) {
        const minutes = (exitTime - ticket.entryTime) / 60000; // ms to minutes
        let hours = Math.ceil(minutes / 60);
        if (hours === 0) hours = 1; // minimum 1 hour

        const rate = this.#rates[ticket.vehicle.type] ?? 20;
        return hours * rate;
    }
}

FlatRatePricing.java

A simpler strategy - flat fee per vehicle type regardless of how long they park. Useful for mall parking (“₹50 for the visit”) or airport short-term. Demonstrates that strategies can have completely different logic while sharing the same interface.

package parkinglot.pricing;

import parkinglot.model.Ticket;
import parkinglot.model.VehicleType;

import java.time.LocalDateTime;
import java.util.EnumMap;
import java.util.Map;

/**
 * Flat fee regardless of duration.
 * Good for mall parking, airport short-term, etc.
 */
public class FlatRatePricing implements PricingStrategy {
    private final Map<VehicleType, Double> flatRates;

    public FlatRatePricing() {
        flatRates = new EnumMap<>(VehicleType.class);
        flatRates.put(VehicleType.MOTORCYCLE, 20.0);
        flatRates.put(VehicleType.CAR, 50.0);
        flatRates.put(VehicleType.TRUCK, 100.0);
    }

    public FlatRatePricing(double motorcycleRate, double carRate, double truckRate) {
        flatRates = new EnumMap<>(VehicleType.class);
        flatRates.put(VehicleType.MOTORCYCLE, motorcycleRate);
        flatRates.put(VehicleType.CAR, carRate);
        flatRates.put(VehicleType.TRUCK, truckRate);
    }

    @Override
    public double calculateFee(Ticket ticket, LocalDateTime exitTime) {
        return flatRates.getOrDefault(ticket.getVehicle().getType(), 50.0);
    }
}
from datetime import datetime

class FlatRatePricing(PricingStrategy):
    """
    Flat fee regardless of duration.
    Good for mall parking, airport short-term, etc.
    """

    def __init__(self, motorcycle_rate: float = 20.0,
                 car_rate: float = 50.0, truck_rate: float = 100.0):
        self._flat_rates = {
            VehicleType.MOTORCYCLE: motorcycle_rate,
            VehicleType.CAR: car_rate,
            VehicleType.TRUCK: truck_rate,
        }

    def calculate_fee(self, ticket: Ticket, exit_time: datetime) -> float:
        return self._flat_rates.get(ticket.vehicle.type, 50.0)
#pragma once
#include <unordered_map>
#include "PricingStrategy.hpp"

/**
 * Flat fee regardless of duration.
 * Good for mall parking, airport short-term, etc.
 */
class FlatRatePricing : public PricingStrategy {
private:
    std::unordered_map<int, double> flatRates;

public:
    FlatRatePricing() {
        flatRates[static_cast<int>(VehicleType::MOTORCYCLE)] = 20.0;
        flatRates[static_cast<int>(VehicleType::CAR)] = 50.0;
        flatRates[static_cast<int>(VehicleType::TRUCK)] = 100.0;
    }

    FlatRatePricing(double motorcycleRate, double carRate, double truckRate) {
        flatRates[static_cast<int>(VehicleType::MOTORCYCLE)] = motorcycleRate;
        flatRates[static_cast<int>(VehicleType::CAR)] = carRate;
        flatRates[static_cast<int>(VehicleType::TRUCK)] = truckRate;
    }

    double calculateFee(const Ticket& ticket, TimePoint exitTime) const override {
        int key = static_cast<int>(ticket.getVehicle().getType());
        return flatRates.count(key) ? flatRates.at(key) : 50.0;
    }
};
/**
 * Flat fee regardless of duration.
 * Good for mall parking, airport short-term, etc.
 */
class FlatRatePricing extends PricingStrategy {
    #flatRates;

    constructor(motorcycleRate = 20, carRate = 50, truckRate = 100) {
        super();
        this.#flatRates = {
            [VehicleType.MOTORCYCLE]: motorcycleRate,
            [VehicleType.CAR]: carRate,
            [VehicleType.TRUCK]: truckRate,
        };
    }

    calculateFee(ticket, exitTime) {
        return this.#flatRates[ticket.vehicle.type] ?? 50;
    }
}

WeekendPricing.java (Extension example)

This wraps any existing strategy and applies a multiplier on weekends.

💡 Decorator pattern = wrap an existing object to add behavior without modifying it. Here we layer “2x on weekends” on top of any base pricing strategy - composable and open for extension.

This shows how Strategy + Decorator combine: you can do new WeekendPricing(new HourlyPricing()) to get hourly rates that double on Saturdays/Sundays.

package parkinglot.pricing;

import parkinglot.model.Ticket;

import java.time.DayOfWeek;
import java.time.LocalDateTime;

/**
 * Double rate on weekends. Delegates to base strategy for calculation.
 * Demonstrates Decorator pattern on top of Strategy.
 */
public class WeekendPricing implements PricingStrategy {
    private final PricingStrategy baseStrategy;
    private final double weekendMultiplier;

    public WeekendPricing(PricingStrategy baseStrategy, double weekendMultiplier) {
        this.baseStrategy = baseStrategy;
        this.weekendMultiplier = weekendMultiplier;
    }

    public WeekendPricing(PricingStrategy baseStrategy) {
        this(baseStrategy, 2.0); // default 2x on weekends
    }

    @Override
    public double calculateFee(Ticket ticket, LocalDateTime exitTime) {
        double baseFee = baseStrategy.calculateFee(ticket, exitTime);
        DayOfWeek day = ticket.getEntryTime().getDayOfWeek();
        if (day == DayOfWeek.SATURDAY || day == DayOfWeek.SUNDAY) {
            return baseFee * weekendMultiplier;
        }
        return baseFee;
    }
}
from datetime import datetime

class WeekendPricing(PricingStrategy):
    """
    Double rate on weekends. Delegates to base strategy for calculation.
    Demonstrates Decorator pattern on top of Strategy.
    """

    def __init__(self, base_strategy: PricingStrategy, weekend_multiplier: float = 2.0):
        self._base_strategy = base_strategy
        self._weekend_multiplier = weekend_multiplier

    def calculate_fee(self, ticket: Ticket, exit_time: datetime) -> float:
        base_fee = self._base_strategy.calculate_fee(ticket, exit_time)
        day = ticket.entry_time.weekday()  # 5=Saturday, 6=Sunday
        if day >= 5:
            return base_fee * self._weekend_multiplier
        return base_fee
#pragma once
#include <memory>
#include <ctime>
#include "PricingStrategy.hpp"

/**
 * Double rate on weekends. Delegates to base strategy for calculation.
 * Demonstrates Decorator pattern on top of Strategy.
 */
class WeekendPricing : public PricingStrategy {
private:
    std::shared_ptr<PricingStrategy> baseStrategy;
    double weekendMultiplier;

    bool isWeekend(TimePoint tp) const {
        auto time_t_val = std::chrono::system_clock::to_time_t(tp);
        std::tm tm_val{};
        localtime_r(&time_t_val, &tm_val);
        return tm_val.tm_wday == 0 || tm_val.tm_wday == 6; // Sunday=0, Saturday=6
    }

public:
    WeekendPricing(std::shared_ptr<PricingStrategy> baseStrategy, double weekendMultiplier = 2.0)
        : baseStrategy(std::move(baseStrategy)), weekendMultiplier(weekendMultiplier) {}

    double calculateFee(const Ticket& ticket, TimePoint exitTime) const override {
        double baseFee = baseStrategy->calculateFee(ticket, exitTime);
        if (isWeekend(ticket.getEntryTime())) {
            return baseFee * weekendMultiplier;
        }
        return baseFee;
    }
};
/**
 * Double rate on weekends. Delegates to base strategy for calculation.
 * Demonstrates Decorator pattern on top of Strategy.
 */
class WeekendPricing extends PricingStrategy {
    #baseStrategy;
    #weekendMultiplier;

    constructor(baseStrategy, weekendMultiplier = 2.0) {
        super();
        this.#baseStrategy = baseStrategy;
        this.#weekendMultiplier = weekendMultiplier;
    }

    calculateFee(ticket, exitTime) {
        const baseFee = this.#baseStrategy.calculateFee(ticket, exitTime);
        const day = ticket.entryTime.getDay(); // 0=Sunday, 6=Saturday
        if (day === 0 || day === 6) {
            return baseFee * this.#weekendMultiplier;
        }
        return baseFee;
    }
}

ParkingLotException.java

A domain-specific unchecked exception for parking operations (lot full, duplicate vehicle, invalid ticket). Gives callers a single exception type to catch for all parking-related failures.

package parkinglot.exception;

public class ParkingLotException extends RuntimeException {
    public ParkingLotException(String message) {
        super(message);
    }
}
class ParkingLotException(RuntimeError):
    """Domain-specific exception for parking operations."""

    def __init__(self, message: str):
        super().__init__(message)
#pragma once
#include <stdexcept>
#include <string>

class ParkingLotException : public std::runtime_error {
public:
    explicit ParkingLotException(const std::string& message)
        : std::runtime_error(message) {}
};
class ParkingLotException extends Error {
    constructor(message) {
        super(message);
        this.name = 'ParkingLotException';
    }
}

ParkingLot.java (Main Controller)

The orchestrator - coordinates floors, tickets, and pricing. Uses ReentrantLock so two concurrent arrivals can’t grab the same spot. The private constructor forces creation through the Builder, which avoids a 6-parameter constructor.

💡 Builder pattern = construct complex objects step-by-step instead of telescoping constructors with 10 parameters. Here: new Builder().name("Mall").addFloor(5,10,3).pricingStrategy(hourly).build()

We maintain two maps: activeTickets (ticketId → Ticket) for O(1) exit lookup, and vehicleTickets (plate → Ticket) for O(1) duplicate detection.

package parkinglot;

import parkinglot.exception.ParkingLotException;
import parkinglot.model.*;
import parkinglot.pricing.HourlyPricing;
import parkinglot.pricing.PricingStrategy;

import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.locks.ReentrantLock;

public class ParkingLot {
    private final String name;
    private final List<Floor> floors;
    private final Map<String, Ticket> activeTickets;   // ticketId → Ticket
    private final Map<String, Ticket> vehicleTickets;  // licensePlate → Ticket
    private PricingStrategy pricingStrategy;
    private final ReentrantLock lock;

    private ParkingLot(String name, List<Floor> floors, PricingStrategy pricingStrategy) {
        this.name = name;
        this.floors = floors;
        this.activeTickets = new HashMap<>();
        this.vehicleTickets = new HashMap<>();
        this.pricingStrategy = pricingStrategy;
        this.lock = new ReentrantLock();
    }

    // ─── Park Vehicle ───────────────────────────────────────

    public Ticket parkVehicle(Vehicle vehicle) {
        lock.lock();
        try {
            // Check if vehicle already parked
            if (vehicleTickets.containsKey(vehicle.getLicensePlate())) {
                throw new ParkingLotException(
                    "Vehicle " + vehicle.getLicensePlate() + " is already parked");
            }

            // Find available spot across all floors
            Spot spot = findSpot(vehicle);
            if (spot == null) {
                throw new ParkingLotException(
                    "No available spot for " + vehicle.getType());
            }

            // Assign spot and create ticket
            spot.assign(vehicle);
            Ticket ticket = new Ticket(vehicle, spot, LocalDateTime.now());
            activeTickets.put(ticket.getId(), ticket);
            vehicleTickets.put(vehicle.getLicensePlate(), ticket);

            return ticket;
        } finally {
            lock.unlock();
        }
    }

    // ─── Unpark Vehicle ─────────────────────────────────────

    public Payment unparkVehicle(String ticketId) {
        return unparkVehicle(ticketId, LocalDateTime.now());
    }

    public Payment unparkVehicle(String ticketId, LocalDateTime exitTime) {
        lock.lock();
        try {
            Ticket ticket = activeTickets.remove(ticketId);
            if (ticket == null) {
                throw new ParkingLotException("Invalid ticket: " + ticketId);
            }

            // Free the spot
            Spot spot = ticket.getSpot();
            spot.free();

            // Return spot to the floor's available pool
            floors.get(spot.getFloorNumber() - 1).freeSpot(spot);

            // Remove vehicle tracking
            vehicleTickets.remove(ticket.getVehicle().getLicensePlate());

            // Calculate fee
            double fee = pricingStrategy.calculateFee(ticket, exitTime);
            long hours = java.time.temporal.ChronoUnit.HOURS.between(
                ticket.getEntryTime(), exitTime) + 1;

            return new Payment(ticket, fee, hours);
        } finally {
            lock.unlock();
        }
    }

    // ─── Spot Finder ────────────────────────────────────────

    private Spot findSpot(Vehicle vehicle) {
        for (Floor floor : floors) {
            Spot spot = floor.getAvailableSpot(vehicle);
            if (spot != null) return spot;
        }
        return null;
    }

    // ─── Pricing Strategy (Runtime Swap) ────────────────────

    public void setPricingStrategy(PricingStrategy strategy) {
        lock.lock();
        try {
            this.pricingStrategy = strategy;
        } finally {
            lock.unlock();
        }
    }

    // ─── Status / Getters ───────────────────────────────────

    public int getTotalSpots() {
        return floors.stream().mapToInt(Floor::getTotalCount).sum();
    }

    public int getAvailableSpots() {
        return floors.stream().mapToInt(Floor::getAvailableCount).sum();
    }

    public int getOccupiedSpots() {
        return getTotalSpots() - getAvailableSpots();
    }

    public String getName() { return name; }
    public List<Floor> getFloors() { return Collections.unmodifiableList(floors); }
    public int getActiveTicketCount() { return activeTickets.size(); }

    public void displayStatus() {
        System.out.println("\n╔══════════════════════════════════════╗");
        System.out.println("║  " + name);
        System.out.println("╠══════════════════════════════════════╣");
        System.out.printf("║  Total: %d | Available: %d | Occupied: %d%n",
                getTotalSpots(), getAvailableSpots(), getOccupiedSpots());
        System.out.println("╠══════════════════════════════════════╣");
        for (Floor floor : floors) {
            System.out.printf("║  Floor %d: %d/%d available%n",
                    floor.getFloorNumber(), floor.getAvailableCount(), floor.getTotalCount());
        }
        System.out.println("╚══════════════════════════════════════╝");
    }

    // ─── Builder ────────────────────────────────────────────

    public static class Builder {
        private String name = "Parking Lot";
        private final List<Floor> floors = new ArrayList<>();
        private PricingStrategy pricingStrategy = new HourlyPricing();
        private int floorCounter = 0;

        public Builder name(String name) {
            this.name = name;
            return this;
        }

        public Builder addFloor(int smallSpots, int mediumSpots, int largeSpots) {
            floorCounter++;
            floors.add(new Floor(floorCounter, smallSpots, mediumSpots, largeSpots));
            return this;
        }

        public Builder pricingStrategy(PricingStrategy strategy) {
            this.pricingStrategy = strategy;
            return this;
        }

        public ParkingLot build() {
            if (floors.isEmpty()) {
                throw new IllegalStateException("Parking lot must have at least one floor");
            }
            return new ParkingLot(name, floors, pricingStrategy);
        }
    }
}
import threading
from datetime import datetime

class ParkingLot:
    def __init__(self, name: str, floors: list[Floor], pricing_strategy: PricingStrategy):
        self._name = name
        self._floors = floors
        self._active_tickets: dict[str, Ticket] = {}    # ticketId -> Ticket
        self._vehicle_tickets: dict[str, Ticket] = {}   # licensePlate -> Ticket
        self._pricing_strategy = pricing_strategy
        self._lock = threading.Lock()

    # ─── Park Vehicle ───────────────────────────────────────

    def park_vehicle(self, vehicle: Vehicle) -> Ticket:
        with self._lock:
            # Check if vehicle already parked
            if vehicle.license_plate in self._vehicle_tickets:
                raise ParkingLotException(
                    f"Vehicle {vehicle.license_plate} is already parked")

            # Find available spot across all floors
            spot = self._find_spot(vehicle)
            if spot is None:
                raise ParkingLotException(
                    f"No available spot for {vehicle.type.value}")

            # Assign spot and create ticket
            spot.assign(vehicle)
            ticket = Ticket(vehicle, spot, datetime.now())
            self._active_tickets[ticket.id] = ticket
            self._vehicle_tickets[vehicle.license_plate] = ticket

            return ticket

    # ─── Unpark Vehicle ─────────────────────────────────────

    def unpark_vehicle(self, ticket_id: str, exit_time: datetime | None = None) -> Payment:
        if exit_time is None:
            exit_time = datetime.now()

        with self._lock:
            ticket = self._active_tickets.pop(ticket_id, None)
            if ticket is None:
                raise ParkingLotException(f"Invalid ticket: {ticket_id}")

            # Free the spot
            spot = ticket.spot
            spot.free()

            # Return spot to the floor's available pool
            self._floors[spot.floor_number - 1].free_spot(spot)

            # Remove vehicle tracking
            self._vehicle_tickets.pop(ticket.vehicle.license_plate, None)

            # Calculate fee
            fee = self._pricing_strategy.calculate_fee(ticket, exit_time)
            hours = int((exit_time - ticket.entry_time).total_seconds() / 3600) + 1

            return Payment(ticket, fee, hours)

    # ─── Spot Finder ────────────────────────────────────────

    def _find_spot(self, vehicle: Vehicle) -> Spot | None:
        for floor in self._floors:
            spot = floor.get_available_spot(vehicle)
            if spot is not None:
                return spot
        return None

    # ─── Pricing Strategy (Runtime Swap) ────────────────────

    def set_pricing_strategy(self, strategy: PricingStrategy) -> None:
        with self._lock:
            self._pricing_strategy = strategy

    # ─── Status / Getters ───────────────────────────────────

    @property
    def total_spots(self) -> int:
        return sum(f.total_count for f in self._floors)

    @property
    def available_spots(self) -> int:
        return sum(f.available_count for f in self._floors)

    @property
    def occupied_spots(self) -> int:
        return self.total_spots - self.available_spots

    @property
    def name(self) -> str:
        return self._name

    @property
    def floors(self) -> list[Floor]:
        return list(self._floors)

    @property
    def active_ticket_count(self) -> int:
        return len(self._active_tickets)

    def display_status(self) -> None:
        print(f"\n╔══════════════════════════════════════╗")
        print(f"║  {self._name}")
        print(f"╠══════════════════════════════════════╣")
        print(f"║  Total: {self.total_spots} | Available: {self.available_spots} | Occupied: {self.occupied_spots}")
        print(f"╠══════════════════════════════════════╣")
        for floor in self._floors:
            print(f"║  Floor {floor.floor_number}: {floor.available_count}/{floor.total_count} available")
        print(f"╚══════════════════════════════════════╝")

    # ─── Builder ────────────────────────────────────────────

    class Builder:
        def __init__(self):
            self._name = "Parking Lot"
            self._floors: list[Floor] = []
            self._pricing_strategy: PricingStrategy = HourlyPricing()
            self._floor_counter = 0

        def name(self, name: str) -> "ParkingLot.Builder":
            self._name = name
            return self

        def add_floor(self, small_spots: int, medium_spots: int, large_spots: int) -> "ParkingLot.Builder":
            self._floor_counter += 1
            self._floors.append(Floor(self._floor_counter, small_spots, medium_spots, large_spots))
            return self

        def pricing_strategy(self, strategy: PricingStrategy) -> "ParkingLot.Builder":
            self._pricing_strategy = strategy
            return self

        def build(self) -> "ParkingLot":
            if not self._floors:
                raise ValueError("Parking lot must have at least one floor")
            return ParkingLot(self._name, self._floors, self._pricing_strategy)
#pragma once
#include <string>
#include <vector>
#include <unordered_map>
#include <mutex>
#include <memory>
#include <iostream>
#include <iomanip>
#include "Floor.hpp"
#include "Ticket.hpp"
#include "Payment.hpp"
#include "PricingStrategy.hpp"
#include "HourlyPricing.hpp"
#include "ParkingLotException.hpp"

class ParkingLot {
private:
    std::string name;
    std::vector<Floor> floors;
    std::unordered_map<std::string, Ticket> activeTickets;   // ticketId -> Ticket
    std::unordered_map<std::string, Ticket> vehicleTickets;  // licensePlate -> Ticket
    std::shared_ptr<PricingStrategy> pricingStrategy;
    mutable std::mutex mtx;

    ParkingLot(std::string name, std::vector<Floor> floors,
               std::shared_ptr<PricingStrategy> pricingStrategy)
        : name(std::move(name)), floors(std::move(floors)),
          pricingStrategy(std::move(pricingStrategy)) {}

    Spot* findSpot(const Vehicle& vehicle) {
        for (auto& floor : floors) {
            Spot* spot = floor.getAvailableSpot(vehicle);
            if (spot != nullptr) return spot;
        }
        return nullptr;
    }

public:
    // ─── Park Vehicle ───────────────────────────────────────

    Ticket parkVehicle(const Vehicle& vehicle) {
        std::lock_guard<std::mutex> lock(mtx);

        // Check if vehicle already parked
        if (vehicleTickets.count(vehicle.getLicensePlate())) {
            throw ParkingLotException(
                "Vehicle " + vehicle.getLicensePlate() + " is already parked");
        }

        // Find available spot across all floors
        Spot* spot = findSpot(vehicle);
        if (spot == nullptr) {
            throw ParkingLotException("No available spot for vehicle");
        }

        // Assign spot and create ticket
        spot->assign(vehicle);
        Ticket ticket(vehicle, spot, std::chrono::system_clock::now());
        activeTickets.emplace(ticket.getId(), ticket);
        vehicleTickets.emplace(vehicle.getLicensePlate(), ticket);

        return ticket;
    }

    // ─── Unpark Vehicle ─────────────────────────────────────

    Payment unparkVehicle(const std::string& ticketId,
                          TimePoint exitTime = std::chrono::system_clock::now()) {
        std::lock_guard<std::mutex> lock(mtx);

        auto it = activeTickets.find(ticketId);
        if (it == activeTickets.end()) {
            throw ParkingLotException("Invalid ticket: " + ticketId);
        }

        Ticket ticket = it->second;
        activeTickets.erase(it);

        // Free the spot
        Spot* spot = ticket.getSpot();
        spot->free();

        // Return spot to the floor's available pool
        floors[spot->getFloorNumber() - 1].freeSpot(spot);

        // Remove vehicle tracking
        vehicleTickets.erase(ticket.getVehicle().getLicensePlate());

        // Calculate fee
        double fee = pricingStrategy->calculateFee(ticket, exitTime);
        auto duration = std::chrono::duration_cast<std::chrono::hours>(
            exitTime - ticket.getEntryTime());
        long hours = duration.count() + 1;

        return Payment(ticket, fee, hours);
    }

    // ─── Pricing Strategy (Runtime Swap) ────────────────────

    void setPricingStrategy(std::shared_ptr<PricingStrategy> strategy) {
        std::lock_guard<std::mutex> lock(mtx);
        pricingStrategy = std::move(strategy);
    }

    // ─── Status / Getters ───────────────────────────────────

    int getTotalSpots() const {
        int total = 0;
        for (const auto& floor : floors) total += floor.getTotalCount();
        return total;
    }

    int getAvailableSpots() const {
        int available = 0;
        for (const auto& floor : floors) available += floor.getAvailableCount();
        return available;
    }

    int getOccupiedSpots() const { return getTotalSpots() - getAvailableSpots(); }
    const std::string& getName() const { return name; }
    int getActiveTicketCount() const { return static_cast<int>(activeTickets.size()); }

    void displayStatus() const {
        std::cout << "\n+======================================+\n";
        std::cout << "|  " << name << "\n";
        std::cout << "+======================================+\n";
        std::cout << "|  Total: " << getTotalSpots()
                  << " | Available: " << getAvailableSpots()
                  << " | Occupied: " << getOccupiedSpots() << "\n";
        std::cout << "+======================================+\n";
        for (const auto& floor : floors) {
            std::cout << "|  Floor " << floor.getFloorNumber()
                      << ": " << floor.getAvailableCount()
                      << "/" << floor.getTotalCount() << " available\n";
        }
        std::cout << "+======================================+\n";
    }

    // ─── Builder ────────────────────────────────────────────

    class Builder {
    private:
        std::string name = "Parking Lot";
        std::vector<Floor> floors;
        std::shared_ptr<PricingStrategy> pricingStrategy = std::make_shared<HourlyPricing>();
        int floorCounter = 0;

    public:
        Builder& setName(const std::string& n) { name = n; return *this; }

        Builder& addFloor(int smallSpots, int mediumSpots, int largeSpots) {
            floorCounter++;
            floors.emplace_back(floorCounter, smallSpots, mediumSpots, largeSpots);
            return *this;
        }

        Builder& setPricingStrategy(std::shared_ptr<PricingStrategy> strategy) {
            pricingStrategy = std::move(strategy);
            return *this;
        }

        ParkingLot build() {
            if (floors.empty()) {
                throw std::runtime_error("Parking lot must have at least one floor");
            }
            return ParkingLot(name, std::move(floors), pricingStrategy);
        }
    };
};
class ParkingLot {
    #name;
    #floors;
    #activeTickets;
    #vehicleTickets;
    #pricingStrategy;

    constructor(name, floors, pricingStrategy) {
        this.#name = name;
        this.#floors = floors;
        this.#activeTickets = new Map();    // ticketId -> Ticket
        this.#vehicleTickets = new Map();   // licensePlate -> Ticket
        this.#pricingStrategy = pricingStrategy;
    }

    // ─── Park Vehicle ───────────────────────────────────────

    parkVehicle(vehicle) {
        // Check if vehicle already parked
        if (this.#vehicleTickets.has(vehicle.licensePlate)) {
            throw new ParkingLotException(
                `Vehicle ${vehicle.licensePlate} is already parked`);
        }

        // Find available spot across all floors
        const spot = this.#findSpot(vehicle);
        if (spot === null) {
            throw new ParkingLotException(
                `No available spot for ${vehicle.type}`);
        }

        // Assign spot and create ticket
        spot.assign(vehicle);
        const ticket = new Ticket(vehicle, spot, new Date());
        this.#activeTickets.set(ticket.id, ticket);
        this.#vehicleTickets.set(vehicle.licensePlate, ticket);

        return ticket;
    }

    // ─── Unpark Vehicle ─────────────────────────────────────

    unparkVehicle(ticketId, exitTime = new Date()) {
        const ticket = this.#activeTickets.get(ticketId);
        if (!ticket) {
            throw new ParkingLotException(`Invalid ticket: ${ticketId}`);
        }
        this.#activeTickets.delete(ticketId);

        // Free the spot
        const spot = ticket.spot;
        spot.free();

        // Return spot to the floor's available pool
        this.#floors[spot.floorNumber - 1].freeSpot(spot);

        // Remove vehicle tracking
        this.#vehicleTickets.delete(ticket.vehicle.licensePlate);

        // Calculate fee
        const fee = this.#pricingStrategy.calculateFee(ticket, exitTime);
        const hours = Math.floor((exitTime - ticket.entryTime) / 3600000) + 1;

        return new Payment(ticket, fee, hours);
    }

    // ─── Spot Finder ────────────────────────────────────────

    #findSpot(vehicle) {
        for (const floor of this.#floors) {
            const spot = floor.getAvailableSpot(vehicle);
            if (spot !== null) return spot;
        }
        return null;
    }

    // ─── Pricing Strategy (Runtime Swap) ────────────────────

    setPricingStrategy(strategy) {
        this.#pricingStrategy = strategy;
    }

    // ─── Status / Getters ───────────────────────────────────

    get totalSpots() {
        return this.#floors.reduce((sum, f) => sum + f.totalCount, 0);
    }

    get availableSpots() {
        return this.#floors.reduce((sum, f) => sum + f.availableCount, 0);
    }

    get occupiedSpots() {
        return this.totalSpots - this.availableSpots;
    }

    get name() { return this.#name; }
    get floors() { return [...this.#floors]; }
    get activeTicketCount() { return this.#activeTickets.size; }

    displayStatus() {
        console.log(`\n╔══════════════════════════════════════╗`);
        console.log(`║  ${this.#name}`);
        console.log(`╠══════════════════════════════════════╣`);
        console.log(`║  Total: ${this.totalSpots} | Available: ${this.availableSpots} | Occupied: ${this.occupiedSpots}`);
        console.log(`╠══════════════════════════════════════╣`);
        for (const floor of this.#floors) {
            console.log(`║  Floor ${floor.floorNumber}: ${floor.availableCount}/${floor.totalCount} available`);
        }
        console.log(`╚══════════════════════════════════════╝`);
    }

    // ─── Builder ────────────────────────────────────────────

    static Builder = class {
        #name = 'Parking Lot';
        #floors = [];
        #pricingStrategy = new HourlyPricing();
        #floorCounter = 0;

        name(name) { this.#name = name; return this; }

        addFloor(smallSpots, mediumSpots, largeSpots) {
            this.#floorCounter++;
            this.#floors.push(new Floor(this.#floorCounter, smallSpots, mediumSpots, largeSpots));
            return this;
        }

        pricingStrategy(strategy) { this.#pricingStrategy = strategy; return this; }

        build() {
            if (this.#floors.length === 0) {
                throw new Error('Parking lot must have at least one floor');
            }
            return new ParkingLot(this.#name, this.#floors, this.#pricingStrategy);
        }
    };
}

Demo.java (Runnable end-to-end)

The demo proves the system works end-to-end: builds a lot, parks vehicles, handles duplicates, unparks with different pricing strategies, and demonstrates thread-safety with concurrent parking from two threads.

package parkinglot;

import parkinglot.model.*;
import parkinglot.pricing.*;

import java.time.LocalDateTime;

public class Demo {
    public static void main(String[] args) {
        System.out.println("═══════════════════════════════════════");
        System.out.println("     PARKING LOT - LLD DEMO           ");
        System.out.println("═══════════════════════════════════════\n");

        // ─── Build Parking Lot ──────────────────────────────
        ParkingLot lot = new ParkingLot.Builder()
                .name("Phoenix Mall Parking")
                .addFloor(5, 10, 3)   // Floor 1: 5 small, 10 medium, 3 large
                .addFloor(5, 10, 3)   // Floor 2: same layout
                .addFloor(0, 5, 5)    // Floor 3: no small, 5 medium, 5 large
                .pricingStrategy(new HourlyPricing()) // ₹10/₹20/₹30 per hour
                .build();

        lot.displayStatus();

        // ─── Park Vehicles ──────────────────────────────────
        System.out.println("\n--- Parking Vehicles ---");

        Vehicle bike1 = new Vehicle("KA-01-1234", VehicleType.MOTORCYCLE);
        Vehicle car1 = new Vehicle("MH-12-AB-1234", VehicleType.CAR);
        Vehicle car2 = new Vehicle("DL-05-CD-5678", VehicleType.CAR);
        Vehicle truck1 = new Vehicle("TN-22-XY-9999", VehicleType.TRUCK);

        Ticket t1 = lot.parkVehicle(bike1);
        System.out.println("✓ Parked: " + t1);

        Ticket t2 = lot.parkVehicle(car1);
        System.out.println("✓ Parked: " + t2);

        Ticket t3 = lot.parkVehicle(car2);
        System.out.println("✓ Parked: " + t3);

        Ticket t4 = lot.parkVehicle(truck1);
        System.out.println("✓ Parked: " + t4);

        lot.displayStatus();

        // ─── Try Duplicate Park ─────────────────────────────
        System.out.println("\n--- Try Parking Same Vehicle Again ---");
        try {
            lot.parkVehicle(car1);
        } catch (Exception e) {
            System.out.println("✗ Expected error: " + e.getMessage());
        }

        // ─── Unpark with Hourly Pricing ─────────────────────
        System.out.println("\n--- Unpark (Hourly Pricing) ---");

        // Simulate 3 hours later
        LocalDateTime threeHoursLater = LocalDateTime.now().plusHours(3);

        Payment p1 = lot.unparkVehicle(t1.getId(), threeHoursLater);
        System.out.println("✓ " + p1);

        Payment p2 = lot.unparkVehicle(t2.getId(), threeHoursLater);
        System.out.println("✓ " + p2);

        // ─── Switch to Flat Rate Pricing ────────────────────
        System.out.println("\n--- Switch to Flat Rate Pricing ---");
        lot.setPricingStrategy(new FlatRatePricing()); // ₹20/₹50/₹100

        Payment p3 = lot.unparkVehicle(t3.getId(), threeHoursLater);
        System.out.println("✓ " + p3);

        // ─── Weekend Pricing (Decorator on Hourly) ──────────
        System.out.println("\n--- Switch to Weekend Pricing (2x Hourly) ---");
        lot.setPricingStrategy(new WeekendPricing(new HourlyPricing(), 2.0));

        Payment p4 = lot.unparkVehicle(t4.getId(), threeHoursLater);
        System.out.println("✓ " + p4);

        lot.displayStatus();

        // ─── Concurrent Access Demo ─────────────────────────
        System.out.println("\n--- Concurrent Parking (Thread Safety) ---");

        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                try {
                    Vehicle v = new Vehicle("T1-" + i, VehicleType.CAR);
                    Ticket t = lot.parkVehicle(v);
                    System.out.println("  [T1] Parked: " + v.getLicensePlate());
                } catch (Exception e) {
                    System.out.println("  [T1] " + e.getMessage());
                }
            }
        });

        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                try {
                    Vehicle v = new Vehicle("T2-" + i, VehicleType.CAR);
                    Ticket t = lot.parkVehicle(v);
                    System.out.println("  [T2] Parked: " + v.getLicensePlate());
                } catch (Exception e) {
                    System.out.println("  [T2] " + e.getMessage());
                }
            }
        });

        thread1.start();
        thread2.start();
        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException ignored) {}

        System.out.println("\nBoth threads parked without race conditions.");
        lot.displayStatus();

        System.out.println("\n═══════════════════════════════════════");
        System.out.println("           DEMO COMPLETE               ");
        System.out.println("═══════════════════════════════════════");
    }
}
import threading
from datetime import datetime, timedelta

def main():
    print("═══════════════════════════════════════")
    print("     PARKING LOT - LLD DEMO           ")
    print("═══════════════════════════════════════\n")

    # ─── Build Parking Lot ──────────────────────────────
    lot = (ParkingLot.Builder()
           .name("Phoenix Mall Parking")
           .add_floor(5, 10, 3)    # Floor 1: 5 small, 10 medium, 3 large
           .add_floor(5, 10, 3)    # Floor 2: same layout
           .add_floor(0, 5, 5)     # Floor 3: no small, 5 medium, 5 large
           .pricing_strategy(HourlyPricing())  # ₹10/₹20/₹30 per hour
           .build())

    lot.display_status()

    # ─── Park Vehicles ──────────────────────────────────
    print("\n--- Parking Vehicles ---")

    bike1 = Vehicle("KA-01-1234", VehicleType.MOTORCYCLE)
    car1 = Vehicle("MH-12-AB-1234", VehicleType.CAR)
    car2 = Vehicle("DL-05-CD-5678", VehicleType.CAR)
    truck1 = Vehicle("TN-22-XY-9999", VehicleType.TRUCK)

    t1 = lot.park_vehicle(bike1)
    print(f"✓ Parked: {t1}")

    t2 = lot.park_vehicle(car1)
    print(f"✓ Parked: {t2}")

    t3 = lot.park_vehicle(car2)
    print(f"✓ Parked: {t3}")

    t4 = lot.park_vehicle(truck1)
    print(f"✓ Parked: {t4}")

    lot.display_status()

    # ─── Try Duplicate Park ─────────────────────────────
    print("\n--- Try Parking Same Vehicle Again ---")
    try:
        lot.park_vehicle(car1)
    except ParkingLotException as e:
        print(f"✗ Expected error: {e}")

    # ─── Unpark with Hourly Pricing ─────────────────────
    print("\n--- Unpark (Hourly Pricing) ---")

    # Simulate 3 hours later
    three_hours_later = datetime.now() + timedelta(hours=3)

    p1 = lot.unpark_vehicle(t1.id, three_hours_later)
    print(f"✓ {p1}")

    p2 = lot.unpark_vehicle(t2.id, three_hours_later)
    print(f"✓ {p2}")

    # ─── Switch to Flat Rate Pricing ────────────────────
    print("\n--- Switch to Flat Rate Pricing ---")
    lot.set_pricing_strategy(FlatRatePricing())  # ₹20/₹50/₹100

    p3 = lot.unpark_vehicle(t3.id, three_hours_later)
    print(f"✓ {p3}")

    # ─── Weekend Pricing (Decorator on Hourly) ──────────
    print("\n--- Switch to Weekend Pricing (2x Hourly) ---")
    lot.set_pricing_strategy(WeekendPricing(HourlyPricing(), 2.0))

    p4 = lot.unpark_vehicle(t4.id, three_hours_later)
    print(f"✓ {p4}")

    lot.display_status()

    # ─── Concurrent Access Demo ─────────────────────────
    print("\n--- Concurrent Parking (Thread Safety) ---")

    def park_batch(prefix: str):
        for i in range(5):
            try:
                v = Vehicle(f"{prefix}-{i}", VehicleType.CAR)
                lot.park_vehicle(v)
                print(f"  [{prefix}] Parked: {v.license_plate}")
            except ParkingLotException as e:
                print(f"  [{prefix}] {e}")

    thread1 = threading.Thread(target=park_batch, args=("T1",))
    thread2 = threading.Thread(target=park_batch, args=("T2",))

    thread1.start()
    thread2.start()
    thread1.join()
    thread2.join()

    print("\nBoth threads parked without race conditions.")
    lot.display_status()

    print("\n═══════════════════════════════════════")
    print("           DEMO COMPLETE               ")
    print("═══════════════════════════════════════")


if __name__ == "__main__":
    main()
#include <iostream>
#include <thread>
#include <chrono>
#include "ParkingLot.hpp"
#include "HourlyPricing.hpp"
#include "FlatRatePricing.hpp"
#include "WeekendPricing.hpp"

int main() {
    std::cout << "═══════════════════════════════════════\n";
    std::cout << "     PARKING LOT - LLD DEMO           \n";
    std::cout << "═══════════════════════════════════════\n\n";

    // ─── Build Parking Lot ──────────────────────────────
    auto lot = ParkingLot::Builder()
        .setName("Phoenix Mall Parking")
        .addFloor(5, 10, 3)    // Floor 1: 5 small, 10 medium, 3 large
        .addFloor(5, 10, 3)    // Floor 2: same layout
        .addFloor(0, 5, 5)     // Floor 3: no small, 5 medium, 5 large
        .setPricingStrategy(std::make_shared<HourlyPricing>())
        .build();

    lot.displayStatus();

    // ─── Park Vehicles ──────────────────────────────────
    std::cout << "\n--- Parking Vehicles ---\n";

    Vehicle bike1("KA-01-1234", VehicleType::MOTORCYCLE);
    Vehicle car1("MH-12-AB-1234", VehicleType::CAR);
    Vehicle car2("DL-05-CD-5678", VehicleType::CAR);
    Vehicle truck1("TN-22-XY-9999", VehicleType::TRUCK);

    auto t1 = lot.parkVehicle(bike1);
    std::cout << "Parked: " << t1.toString() << "\n";

    auto t2 = lot.parkVehicle(car1);
    std::cout << "Parked: " << t2.toString() << "\n";

    auto t3 = lot.parkVehicle(car2);
    std::cout << "Parked: " << t3.toString() << "\n";

    auto t4 = lot.parkVehicle(truck1);
    std::cout << "Parked: " << t4.toString() << "\n";

    lot.displayStatus();

    // ─── Try Duplicate Park ─────────────────────────────
    std::cout << "\n--- Try Parking Same Vehicle Again ---\n";
    try {
        lot.parkVehicle(car1);
    } catch (const ParkingLotException& e) {
        std::cout << "Expected error: " << e.what() << "\n";
    }

    // ─── Unpark with Hourly Pricing ─────────────────────
    std::cout << "\n--- Unpark (Hourly Pricing) ---\n";

    auto threeHoursLater = std::chrono::system_clock::now() + std::chrono::hours(3);

    auto p1 = lot.unparkVehicle(t1.getId(), threeHoursLater);
    std::cout << p1.toString() << "\n";

    auto p2 = lot.unparkVehicle(t2.getId(), threeHoursLater);
    std::cout << p2.toString() << "\n";

    // ─── Switch to Flat Rate Pricing ────────────────────
    std::cout << "\n--- Switch to Flat Rate Pricing ---\n";
    lot.setPricingStrategy(std::make_shared<FlatRatePricing>());

    auto p3 = lot.unparkVehicle(t3.getId(), threeHoursLater);
    std::cout << p3.toString() << "\n";

    // ─── Weekend Pricing (Decorator on Hourly) ──────────
    std::cout << "\n--- Switch to Weekend Pricing (2x Hourly) ---\n";
    lot.setPricingStrategy(
        std::make_shared<WeekendPricing>(std::make_shared<HourlyPricing>(), 2.0));

    auto p4 = lot.unparkVehicle(t4.getId(), threeHoursLater);
    std::cout << p4.toString() << "\n";

    lot.displayStatus();

    // ─── Concurrent Access Demo ─────────────────────────
    std::cout << "\n--- Concurrent Parking (Thread Safety) ---\n";

    auto parkBatch = [&lot](const std::string& prefix) {
        for (int i = 0; i < 5; ++i) {
            try {
                Vehicle v(prefix + "-" + std::to_string(i), VehicleType::CAR);
                lot.parkVehicle(v);
                std::cout << "  [" << prefix << "] Parked: " << v.getLicensePlate() << "\n";
            } catch (const ParkingLotException& e) {
                std::cout << "  [" << prefix << "] " << e.what() << "\n";
            }
        }
    };

    std::thread thread1(parkBatch, "T1");
    std::thread thread2(parkBatch, "T2");
    thread1.join();
    thread2.join();

    std::cout << "\nBoth threads parked without race conditions.\n";
    lot.displayStatus();

    std::cout << "\n═══════════════════════════════════════\n";
    std::cout << "           DEMO COMPLETE               \n";
    std::cout << "═══════════════════════════════════════\n";

    return 0;
}
function main() {
    console.log("═══════════════════════════════════════");
    console.log("     PARKING LOT - LLD DEMO           ");
    console.log("═══════════════════════════════════════\n");

    // ─── Build Parking Lot ──────────────────────────────
    const lot = new ParkingLot.Builder()
        .name("Phoenix Mall Parking")
        .addFloor(5, 10, 3)    // Floor 1: 5 small, 10 medium, 3 large
        .addFloor(5, 10, 3)    // Floor 2: same layout
        .addFloor(0, 5, 5)     // Floor 3: no small, 5 medium, 5 large
        .pricingStrategy(new HourlyPricing())  // ₹10/₹20/₹30 per hour
        .build();

    lot.displayStatus();

    // ─── Park Vehicles ──────────────────────────────────
    console.log("\n--- Parking Vehicles ---");

    const bike1 = new Vehicle("KA-01-1234", VehicleType.MOTORCYCLE);
    const car1 = new Vehicle("MH-12-AB-1234", VehicleType.CAR);
    const car2 = new Vehicle("DL-05-CD-5678", VehicleType.CAR);
    const truck1 = new Vehicle("TN-22-XY-9999", VehicleType.TRUCK);

    const t1 = lot.parkVehicle(bike1);
    console.log(`✓ Parked: ${t1}`);

    const t2 = lot.parkVehicle(car1);
    console.log(`✓ Parked: ${t2}`);

    const t3 = lot.parkVehicle(car2);
    console.log(`✓ Parked: ${t3}`);

    const t4 = lot.parkVehicle(truck1);
    console.log(`✓ Parked: ${t4}`);

    lot.displayStatus();

    // ─── Try Duplicate Park ─────────────────────────────
    console.log("\n--- Try Parking Same Vehicle Again ---");
    try {
        lot.parkVehicle(car1);
    } catch (e) {
        console.log(`✗ Expected error: ${e.message}`);
    }

    // ─── Unpark with Hourly Pricing ─────────────────────
    console.log("\n--- Unpark (Hourly Pricing) ---");

    // Simulate 3 hours later
    const threeHoursLater = new Date(Date.now() + 3 * 60 * 60 * 1000);

    const p1 = lot.unparkVehicle(t1.id, threeHoursLater);
    console.log(`✓ ${p1}`);

    const p2 = lot.unparkVehicle(t2.id, threeHoursLater);
    console.log(`✓ ${p2}`);

    // ─── Switch to Flat Rate Pricing ────────────────────
    console.log("\n--- Switch to Flat Rate Pricing ---");
    lot.setPricingStrategy(new FlatRatePricing());  // ₹20/₹50/₹100

    const p3 = lot.unparkVehicle(t3.id, threeHoursLater);
    console.log(`✓ ${p3}`);

    // ─── Weekend Pricing (Decorator on Hourly) ──────────
    console.log("\n--- Switch to Weekend Pricing (2x Hourly) ---");
    lot.setPricingStrategy(new WeekendPricing(new HourlyPricing(), 2.0));

    const p4 = lot.unparkVehicle(t4.id, threeHoursLater);
    console.log(`✓ ${p4}`);

    lot.displayStatus();

    // ─── Concurrent Access Note ─────────────────────────
    // JavaScript is single-threaded; concurrency in Node.js is handled
    // via the event loop (async/await), not OS threads.
    // The lock-based design above is still correct for async scenarios
    // where you'd use a mutex/semaphore library for critical sections.

    console.log("\n--- Concurrent Parking (Single-threaded JS) ---");
    for (let i = 0; i < 5; i++) {
        try {
            const v = new Vehicle(`T1-${i}`, VehicleType.CAR);
            lot.parkVehicle(v);
            console.log(`  [T1] Parked: ${v.licensePlate}`);
        } catch (e) {
            console.log(`  [T1] ${e.message}`);
        }
    }
    for (let i = 0; i < 5; i++) {
        try {
            const v = new Vehicle(`T2-${i}`, VehicleType.CAR);
            lot.parkVehicle(v);
            console.log(`  [T2] Parked: ${v.licensePlate}`);
        } catch (e) {
            console.log(`  [T2] ${e.message}`);
        }
    }

    lot.displayStatus();

    console.log("\n═══════════════════════════════════════");
    console.log("           DEMO COMPLETE               ");
    console.log("═══════════════════════════════════════");
}

main();

State Transitions

stateDiagram-v2
    [*] --> AVAILABLE
    AVAILABLE --> OCCUPIED : vehicle parks
    OCCUPIED --> AVAILABLE : vehicle exits

Sequence Diagram - Park Vehicle

sequenceDiagram
    participant Driver
    participant PL as ParkingLot
    participant Lock as ReentrantLock
    participant Floor
    participant Spot
    participant Ticket

    Driver->>PL: parkVehicle(vehicle)
    PL->>Lock: lock()
    PL->>PL: check duplicate vehicle
    PL->>Floor: getAvailableSpot(vehicle)
    Floor->>Floor: check queues by size
    Floor-->>PL: Spot (or null)
    PL->>Spot: assign(vehicle)
    PL->>Ticket: new Ticket(vehicle, spot, now)
    PL->>PL: store in activeTickets map
    PL->>Lock: unlock()
    PL-->>Driver: return Ticket

How to Extend

Extension Implementation
Reserved/VIP spots Add boolean reserved to Spot, filter in getAvailableSpot
EV charging spots New SpotSize.EV_CHARGING or boolean flag on Spot
Display panel Observer pattern - Spot notifies panel on assign/free
Multiple entry/exit gates Gate class that calls ParkingLot.parkVehicle (lock handles concurrency)
Subscription/monthly pass New SubscriptionPricing implements PricingStrategy with ₹0 for passholders
Nearest spot algorithm Priority queue ordered by distance to entry gate
Capacity alerts Observer notified when occupancy exceeds 80%

What Interviewers Look For

  1. Strategy pattern for pricing - not if/else inside ParkingLot
  2. Best-fit spot assignment - motorcycle gets SMALL first, not LARGE
  3. Thread-safety - ReentrantLock prevents double-assignment
  4. O(1) spot retrieval - Queue<Spot> per size, not linear scan
  5. Clean separation - Vehicle doesn’t know about Spot, Spot doesn’t know about pricing
  6. Builder pattern - clean construction of complex ParkingLot
  7. Runnable demo - compiles and runs end-to-end with clear output
  8. Extensibility - new pricing = one class, new vehicle type = add to enum + update canFit

Discussion

Newest first
You

Free system design + DSA prep. If it helped you crack an interview, consider supporting.