Designing a Parking Lot System
Difficulty: Beginner Patterns: Strategy, Factory, Observer, Composition Asked at: Flipkart, PhonePe, Amazon, Google, Uber
Functional Requirements
- Parking lot has multiple floors, each floor has spots of sizes: Small, Medium, Large
- Vehicle types: Motorcycle, Car, Truck
- Motorcycle fits in any spot. Car fits in Medium/Large. Truck needs Large only.
- On entry: assign nearest available matching spot, issue ticket
- On exit: calculate fee based on duration and vehicle type, process payment
- Strategy-based pricing - hourly, flat-rate, or weekend pricing (swappable)
Non-Functional Requirements
- Thread-safety - two vehicles shouldn’t be assigned the same spot concurrently
- O(1) spot lookup - use appropriate data structures for fast spot assignment
- 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:
- Driver enters → system calls
parkVehicle(car) - ParkingLot acquires a lock (thread-safety for concurrent arrivals)
- Checks if car is already parked (duplicate prevention via
vehicleTicketsmap) - Iterates through floors, asking each for an available MEDIUM or LARGE spot
- Floor checks its
Queue<Spot>for the smallest fitting size first (best-fit strategy) - Spot is assigned, ticket is issued with entry timestamp
- Lock is released, ticket returned to driver
When the car leaves:
- Driver presents ticket → system calls
unparkVehicle(ticketId) - ParkingLot acquires lock, looks up ticket in O(1) from
activeTicketsmap - PricingStrategy calculates fee based on duration and vehicle type
- Spot is freed and returned to the floor’s available queue
- 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
- ✅ Strategy pattern for pricing - not if/else inside ParkingLot
- ✅ Best-fit spot assignment - motorcycle gets SMALL first, not LARGE
- ✅ Thread-safety -
ReentrantLockprevents double-assignment - ✅ O(1) spot retrieval -
Queue<Spot>per size, not linear scan - ✅ Clean separation - Vehicle doesn’t know about Spot, Spot doesn’t know about pricing
- ✅ Builder pattern - clean construction of complex ParkingLot
- ✅ Runnable demo - compiles and runs end-to-end with clear output
- ✅ Extensibility - new pricing = one class, new vehicle type = add to enum + update
canFit
Related Designs
- Music Player - Strategy and Observer patterns in action
- Splitwise - Strategy pattern for multiple split algorithms
Discussion
Newest first