"Design a parking lot system. It should support multiple vehicle types and multiple spot types, assign a spot when a vehicle enters, generate a ticket, and compute a fee when the vehicle leaves. Assume it can have multiple floors."
Note what is not specified: the vehicle-to-spot matching rule, the pricing rule, single vs. multiple entrances. A strong candidate surfaces these in section E before writing a single class.
B. Difficulty
Intermediate, escalating to Advanced + Concurrency once the interviewer adds multiple simultaneous entrances (a very common follow-up). Single-floor/single-entrance is a clean intermediate OOD test: enough entities and changing behavior (pricing, allocation, vehicle types) to test abstraction skill, but no distributed-systems concerns.
C. What This Problem Tests
Abstraction — separating "vehicle" and "spot" from their concrete subtypes
Composition over inheritance (Lot has Floors has Spots — not "is-a")
Strategy pattern for allocation and pricing — only once justified by a requirement
Open/Closed Principle — adding a vehicle type or pricing rule without touching existing classes
Facade pattern for a clean external API
Concurrency reasoning once multiple entrances are introduced
D. Requirements
Functional
Support multiple vehicle types (motorcycle, car, bus/truck at minimum)
Support multiple spot types/sizes with a compatibility rule between vehicle and spot
Assign an available compatible spot on entry; reject entry if none available
Issue a ticket at entry (unique ID, entry time, assigned spot)
On exit, validate the ticket, compute a fee based on duration, free the spot
Support multiple floors
Track real-time availability, ideally per spot type
Non-Functional
Extensibility — new vehicle types, spot types, pricing rules should not require touching existing classes
Correctness under concurrent entries — two vehicles must never be assigned the same spot
Interview-scale latency (not "millions of cars/sec" — don't over-engineer)
Out of scope by default: physical hardware (gates/cameras), payment gateway integration, reservations — unless the interviewer asks
E. Clarifying Questions
#
Question
Why It Matters
Design Decision Affected
1
What vehicle types must be supported, and can new ones be added later?
Determines a hardcoded enum vs. an extensible size/category model
Vehicle hierarchy, matching logic
2
Is matching exact type→type, or by a general "size" concept?
Size-based matching scales to new vehicle types with zero new classes
VehicleSize enum vs. type-to-type map
3
Can a spot type hold multiple vehicle sizes (e.g. Large fits a Motorcycle)?
Changes allocation from exact match to "first spot ≥ required size"
Allocation strategy logic
4
Single-floor or multi-floor from day one?
Avoids either premature complexity or a later redesign
Whether a Floor layer exists
5
Flat hourly, tiered, or size-dependent fee?
Determines whether pricing is one formula or needs an abstraction
FeeStrategy vs. inline calc
6
Can pricing change over time (peak hours, promos)?
If yes, pricing must be pluggable, not hardcoded
Strategy pattern justification
7
Are there multiple entrances/exits operating concurrently?
The single biggest driver of concurrency design
Locking granularity, atomicity of allocation
8
Can a spot be reserved in advance?
Adds a reservation lifecycle distinct from walk-in occupancy
Extra state on ParkingSpot
9
What happens when the lot is full?
Simple rejection vs. queue/waitlist
Error-handling contract on parkVehicle
10
Is payment processing in scope, or just fee computation?
Avoids building an unrequested payment gateway integration
Whether PaymentProcessor exists
11
Must state persist across restarts?
Affects ID durability and crash recovery of ticket state
Persistence section relevance
12
Do we need "nearest to entrance," or is any available spot fine?
Nearest-spot needs a different data structure (e.g. per-entrance min-heap)
Allocation data structure
13
Do lost/duplicate tickets need handling?
Affects whether Ticket needs a status/state machine
Assumption: matching is size-based (MOTORCYCLE < COMPACT < LARGE) — a spot fits any vehicle at or below its size. Generalizes better than 1:1 type mapping.
Assumption: single entry/exit path in the base design (v1–v4); concurrency is introduced explicitly in v6, mirroring how interviewers actually escalate this problem.
Assumption: pricing is hourly, rounded up, size-dependent — modeled as a pluggable strategy from the start since the interviewer confirmed it "could change."
Assumption: no reservations, no payment gateway, no hardware modeling in the base design — explicit follow-ups (section V), not baked in prematurely.
Assumption: in-memory state; persistence discussed as an extension (section X).
G. Actors
Actor
Responsibility
Driver / Customer
Initiates entry (brings a vehicle) and exit (presents ticket)
Lot Operator / Admin
Configures floors/spots — administrative, outside the park/unpark hot path
System
Performs allocation, ticketing, fee calculation autonomously — no human decision in the core flow
H. Core Domain Entities
Class
Responsibility
Key Attributes
Key Methods
Does NOT Handle
Vehicle (abstract)
Represents a vehicle and its size class
licensePlate, size
getSize()
Spot assignment, pricing
Motorcycle/Car/Bus
Concrete vehicle — fixes size
—
—
Any behavior beyond identity
ParkingSpot
Represents one physical space
id, size, isOccupied
canFit(), occupy(), vacate()
Knowing about floors, pricing, tickets
Floor
Groups spots, tracks per-floor availability
floorNumber, spots-by-size
findAvailableSpot(size)
Global allocation policy, pricing
SpotAllocator (interface)
Decides which spot to assign across floors
—
allocate(Vehicle)
Occupancy bookkeeping itself
Ticket
Represents one parking session
id, vehicle, spot, entry/exit time, status
close(exitTime)
Fee computation logic
FeeStrategy (interface)
Computes fee for a completed session
—
calculate(Ticket)
Payment collection
ParkingLot (facade)
Public entry point
floors
parkVehicle(), unparkVehicle()
Allocation/pricing logic itself (delegates)
Deliberately not created (yet):PaymentProcessor, EntrancePanel/ExitPanel, DisplayBoard, an Admin account hierarchy. Some source implementations include these, but they aren't justified by the stated requirements — adding them now would be speculative design, not requirement-driven (see AA).
I. Class Relationships
Composition:ParkingLot owns Floors; Floor owns ParkingSpots. A spot cannot outlive its floor conceptually — composition, not aggregation.
Inheritance:Vehicle → Motorcycle/Car/Bus. Kept shallow — only diverges on data (size), not behavior, so it stays justified rather than becoming a taxonomy for its own sake.
Dependency:ParkingLot → SpotAllocator and ParkingLot → FeeStrategy — both injected interfaces, not concrete classes, so the allocation/pricing algorithm can be swapped without touching ParkingLot.
Association:Ticket → Vehicle, Ticket → ParkingSpot — a ticket references them but doesn't own their lifecycle.
Why composition over inheritance for Floor/Spot? A floor is not "a kind of" spot collection with special behavior — it's a container. Modeling it as inheritance would force artificial subclassing just to change floor count, which is a runtime configuration concern, not a type concern.
J. Class Diagram
View Mermaid source
classDiagram
class ParkingLot {
+parkVehicle(Vehicle)
+unparkVehicle(Ticket)
}
class Floor {
+findAvailableSpot(VehicleSize)
}
class ParkingSpot {
+canFit(Vehicle)
+occupy()
+vacate()
}
class SpotAllocator {
<<interface>>
+allocate(Vehicle)
}
class FeeStrategy {
<<interface>>
+calculate(Ticket)
}
class Vehicle {
<<abstract>>
+getSize()
}
class Ticket {
id
entryTime
exitTime
status
}
ParkingLot *-- Floor
Floor *-- ParkingSpot
ParkingLot ..> SpotAllocator
ParkingLot ..> FeeStrategy
SpotAllocator <|.. NearestSpotAllocator
SpotAllocator <|.. FirstAvailableAllocator
FeeStrategy <|.. HourlyFeeStrategy
FeeStrategy <|.. PeakHourFeeStrategy
Vehicle <|-- Motorcycle
Vehicle <|-- Car
Vehicle <|-- Bus
Ticket --> Vehicle
Ticket --> ParkingSpot
Mutating spot state directly (calls spot's own methods)
FeeStrategy
Computes a fee for a session
Collecting/processing payment
Ticket
Records one session's facts
Any computation — it's a data holder
L. SOLID Analysis
Principle
How It's Applied
Naive Violation Example
SRP
ParkingLot only coordinates; pricing and allocation live in their own classes
One ParkingLot class with calculateFee(), findSpot(), processPayment(), printTicket() all inline
OCP
New vehicle type = new Vehicle subclass; new pricing = new FeeStrategy; zero edits to existing classes
if (vehicle.type == "TRUCK") fee = ... else if (...) chains that grow with every new type
LSP
Any Vehicle subtype can be passed to parkVehicle() without special-casing
A subclass that throws "unsupported" for some method the base type promises
ISP
SpotAllocator and FeeStrategy are separate, single-method interfaces — no client is forced to implement methods it doesn't need
One fat ParkingPolicy interface with allocation + pricing + notification methods
DIP
ParkingLot depends on the SpotAllocator/FeeStrategy abstractions, injected at construction — not concrete implementations
ParkingLot directly instantiates new NearestSpotAllocator() internally, making it untestable/unswappable
M. Design Patterns
Pattern
Problem It Solves
Where It Appears
Alternative
When NOT to Use
Strategy
Allocation and pricing logic vary independently and change over time
SpotAllocator, FeeStrategy
If pricing truly never changes: inline a single formula method
If there is exactly one, permanent algorithm — don't add an interface "just in case"
Facade
Callers shouldn't need to know about Floor/Allocator/FeeStrategy wiring
ParkingLot.parkVehicle()/unparkVehicle()
Expose sub-objects directly (leaky abstraction)
If the system is trivially small — facade adds a layer of indirection
State (optional)
Ticket lifecycle: ACTIVE → PAID / LOST
Ticket.status
Plain enum + if/else (fine at this scale)
Don't build a full State-pattern class hierarchy for 2–3 states — an enum is enough until behavior per state gets complex
Factory (light)
Centralize unique ticket-ID / vehicle creation
TicketFactory (optional)
Plain constructor + UUID call
Don't add a Factory class for a one-line new call — over-engineering flagged in AA
Explicitly avoided: Singleton for ParkingLot. Several source implementations default to it "because there's only one lot," but that's an application-wiring decision (create one instance and pass it around), not a class-design constraint — Singleton would make the class untestable (can't spin up two lots in unit tests) for no real benefit.
N. What Is Likely to Change?
Behavior
Current Design
Anticipated Change
Protecting Abstraction
Spot allocation policy
First-available within size
Nearest-to-entrance, load-balanced across floors
SpotAllocator interface
Pricing
Flat hourly by size
Peak hours, subscriptions, weekday/weekend rates
FeeStrategy interface
Vehicle types
Motorcycle/Car/Bus
EV, oversized, handicapped-only
Vehicle subclassing + size enum
Payment method
Not in scope
Cash, card, mobile wallet
Future PaymentStrategy interface (not built prematurely)
Notification
Not in scope
SMS/app alert when lot is nearly full
Future Observer/event bus (not built prematurely)
O. Design Evolution
V1 — Single floor, one vehicle type:ParkingLot directly holds a list of spots.
V2 — Multiple vehicle types + spot types: introduce Vehicle/ParkingSpot abstractions and a size-matching rule.
V3 — Multiple floors: introduce Floor as a composition layer; ParkingLot now iterates floors instead of a flat spot list.
V6 — Concurrent entrances: spot reservation becomes an atomic operation (lock or CAS on the spot/floor); ParkingLot's public API signature doesn't change — only the internals of allocation gain synchronization.
What stays stable across all versions: the ParkingLot.parkVehicle()/unparkVehicle() facade signature. That stability is the payoff of extracting SpotAllocator and FeeStrategy early — callers never notice the internal evolution.
class ParkingLot {
Ticket parkVehicle(Vehicle vehicle) throws NoAvailableSpotException { throw new UnsupportedOperationException(); }
Fee unparkVehicle(Ticket ticket) throws InvalidTicketException { throw new UnsupportedOperationException(); }
}
interface SpotAllocator {
Optional<ParkingSpot> allocate(VehicleSize size); // returns Optional.empty() / throws if none available
}
interface FeeStrategy {
Money calculate(Ticket closedTicket);
}
class ParkingSpot {
boolean canFit(VehicleSize size) { throw new UnsupportedOperationException(); }
void occupy(Vehicle vehicle) { throw new UnsupportedOperationException(); } // throws IllegalStateException if already occupied
void vacate() { throw new UnsupportedOperationException(); }
}
class ParkingLot {
public:
std::shared_ptr<Ticket> parkVehicle(std::shared_ptr<Vehicle> vehicle); // throws NoAvailableSpotException
Fee unparkVehicle(std::shared_ptr<Ticket> ticket); // throws InvalidTicketException
};
class SpotAllocator {
public:
virtual std::optional<ParkingSpot> allocate(VehicleSize size) = 0; // returns nullopt / throws if none available
};
class FeeStrategy {
public:
virtual Money calculate(std::shared_ptr<Ticket> closedTicket) = 0;
};
class ParkingSpot {
public:
bool canFit(VehicleSize size);
void occupy(std::shared_ptr<Vehicle> vehicle); // throws std::logic_error if already occupied
void vacate();
};
class ParkingLot {
parkVehicle(vehicle) { throw new Error('not implemented'); } // throws NoAvailableSpotException
unparkVehicle(ticket) { throw new Error('not implemented'); } // throws InvalidTicketException
}
class SpotAllocator {
allocate(size) { throw new Error('not implemented'); } // returns null / throws if none available
}
class FeeStrategy {
calculate(closedTicket) { throw new Error('not implemented'); }
}
class ParkingSpot {
canFit(size) { throw new Error('not implemented'); }
occupy(vehicle) { throw new Error('not implemented'); } // throws Error if already occupied
vacate() { throw new Error('not implemented'); }
}
class ParkingLot {
parkVehicle(vehicle: Vehicle): Ticket { throw new Error('not implemented'); } // throws NoAvailableSpotException
unparkVehicle(ticket: Ticket): Fee { throw new Error('not implemented'); } // throws InvalidTicketException
}
interface SpotAllocator {
allocate(size: VehicleSize): ParkingSpot | null; // returns null / throws if none available
}
interface FeeStrategy {
calculate(closedTicket: Ticket): Money;
}
class ParkingSpot {
canFit(size: VehicleSize): boolean { throw new Error('not implemented'); }
occupy(vehicle: Vehicle): void { throw new Error('not implemented'); } // throws Error if already occupied
vacate(): void { throw new Error('not implemented'); }
}
Ownership/validation notes:parkVehicle owns spot-reservation atomicity — the caller never sees a half-reserved spot. unparkVehicle must validate ticket status (reject already-closed or unknown tickets) before computing a fee — this is the boundary where most "duplicate exit" bugs occur if skipped.
Q. Pseudocode — Critical Logic
park(vehicle):
size = vehicle.getSize()
spot = allocator.allocate(size) // must be atomic — see W. Concurrency
if spot is null: raise NoAvailableSpotException
spot.occupy(vehicle)
ticket = Ticket.create(vehicle, spot, now())
return ticket
unpark(ticket):
if ticket.status != ACTIVE: raise InvalidTicketException
ticket.close(now())
fee = feeStrategy.calculate(ticket)
ticket.spot.vacate()
return fee
Ticket park(Vehicle vehicle) throws NoAvailableSpotException { // custom checked exception
VehicleSize size = vehicle.getSize();
Optional<ParkingSpot> spot = allocator.allocate(size); // must be atomic — see W. Concurrency
if (spot.isEmpty()) throw new NoAvailableSpotException();
spot.get().occupy(vehicle);
Ticket ticket = Ticket.create(vehicle, spot.get(), now());
return ticket;
}
Fee unpark(Ticket ticket) throws InvalidTicketException { // custom checked exception
if (ticket.status != ACTIVE) throw new InvalidTicketException();
ticket.close(now());
Fee fee = feeStrategy.calculate(ticket);
ticket.spot.vacate();
return fee;
}
std::shared_ptr<Ticket> park(std::shared_ptr<Vehicle> vehicle) {
VehicleSize size = vehicle->getSize();
std::optional<ParkingSpot> spot = allocator.allocate(size); // must be atomic — see W. Concurrency
if (!spot.has_value()) throw NoAvailableSpotException();
spot->occupy(vehicle);
auto ticket = Ticket::create(vehicle, *spot, now());
return ticket;
}
Fee unpark(std::shared_ptr<Ticket> ticket) {
if (ticket->status != ACTIVE) throw InvalidTicketException();
ticket->close(now());
Fee fee = feeStrategy.calculate(ticket);
ticket->spot.vacate();
return fee;
}
function park(vehicle) {
const size = vehicle.getSize();
const spot = allocator.allocate(size); // must be atomic — see W. Concurrency
if (spot === null) throw new Error('NoAvailableSpotException');
spot.occupy(vehicle);
const ticket = Ticket.create(vehicle, spot, now());
return ticket;
}
function unpark(ticket) {
if (ticket.status !== ACTIVE) throw new Error('InvalidTicketException');
ticket.close(now());
const fee = feeStrategy.calculate(ticket);
ticket.spot.vacate();
return fee;
}
function park(vehicle: Vehicle): Ticket {
const size: VehicleSize = vehicle.getSize();
const spot: ParkingSpot | null = allocator.allocate(size); // must be atomic — see W. Concurrency
if (spot === null) throw new Error('NoAvailableSpotException');
spot.occupy(vehicle);
const ticket: Ticket = Ticket.create(vehicle, spot, now());
return ticket;
}
function unpark(ticket: Ticket): Fee {
if (ticket.status !== ACTIVE) throw new Error('InvalidTicketException');
ticket.close(now());
const fee: Fee = feeStrategy.calculate(ticket);
ticket.spot.vacate();
return fee;
}
Single flat list + linear scan — simpler but O(n) per allocation
Map<TicketId, Ticket>
Ticket validation on exit
O(1) lookup by ticket ID
DB-backed lookup if persistence is required (see X)
Min-heap per entrance (only if "nearest spot" is required)
Nearest-available-spot allocation
O(log n) pop of the closest free spot
Plain list + scan — O(n), fine at small scale, avoid the heap unless nearest-spot was explicitly asked for
T. Complexity Analysis
Operation
Time
Space
parkVehicle (first-available)
O(1) amortized with size-bucketed map
O(1) extra per ticket
parkVehicle (nearest-spot via heap)
O(log n)
O(n) for the heap
unparkVehicle
O(1)
O(1)
Overall spot storage
—
O(total spots)
U. Testing Strategy
Happy path: park a car, get a ticket, unpark, get a correct fee.
Edge cases: lot exactly full; vehicle exactly at a spot's size boundary; zero-duration parking (entry==exit time).
Invalid input: unpark with an unknown ticket ID; unpark with an already-closed ticket; park a vehicle type with no compatible spot anywhere.
Failure scenarios: allocator returns a spot that gets raced by another thread (see W); fee strategy misconfigured (negative/zero rate).
Concurrency tests: N threads racing to park when only 1 spot remains — exactly one must succeed, the rest must get NoAvailableSpotException, never a double-allocation.
V. Interviewer Follow-Ups
Follow-Up
Testing
Design Change
Classes Changed
Classes Unchanged
Pattern/Trade-off
"Now support EV charging spots."
OCP under a new requirement
New spot behavior (charge while parked)
New EVSpot subclass (only one whose behavior actually differs)
ParkingLot, SpotAllocator
Subclass only where behavior differs — not every spot type needs one
"Now support reservations."
State modeling
Spot gains a RESERVED state distinct from OCCUPIED; reservation has an expiry
ParkingSpot, new Reservation class
FeeStrategy
Careful state machine — expired reservations must auto-release (a background sweep or lazy check on next allocation)
"Two cars arrive at the same instant for the last spot."
Concurrency / race conditions
Allocation must be atomic (lock-per-floor or CAS on spot state)
ParkingSpot.occupy(), SpotAllocator
External API signature
Optimistic (CAS) vs. pessimistic (lock) — see W
"Pricing changes daily based on demand."
Recognizing Strategy was the right call
New FeeStrategy implementation, no other change
New strategy class only
ParkingLot, Ticket
Validates OCP investment made in V4
"Support millions of parking lots across a city (not just one)."
LLD → HLD boundary awareness
This crosses into HLD: sharding by lot ID, a database instead of in-memory maps
None in-process — this is a service/infra change
All core classes — the domain model doesn't change, only where it runs
Recognize the boundary rather than trying to "distributed-system-ify" the classes themselves
"The payment provider can fail mid-transaction."
Failure handling / idempotency
unparkVehicle must not vacate the spot until payment is confirmed, and must be retry-safe
ParkingLot.unparkVehicle
ParkingSpot
Idempotency key on the fee/payment call
"Add handicapped-only spots that regular cars can't use even if compatible by size."
Rule modeling beyond simple size
canFit() needs an eligibility check beyond size (e.g. a permit flag)
ParkingSpot.canFit(), Vehicle
SpotAllocator logic itself
Don't special-case in the allocator — push the rule into canFit() so allocator stays generic
"What if the same license plate tries to park twice?"
Business-rule edge case
Reject if an ACTIVE ticket already exists for that plate
ParkingLot.parkVehicle
—
Simple guard, not a new class
"Support monthly subscribers with pre-assigned spots."
Spot ownership model
A spot can be reserved-for-owner outside the general pool
ParkingSpot gains an optional owner reference; allocator excludes owned spots from general pool
FeeStrategy
Careful not to let this leak into every allocator call as a special case — filter the pool upstream
"How would you test this without a real clock?"
Testability awareness
Inject a Clock/time source instead of calling now() directly
Ticket, FeeStrategy
—
Dependency injection over static time calls
"What if two floors both report an available compact spot at the same nanosecond?"
Cross-floor race
Locking granularity must be coarse enough to prevent this (floor-level or spot-level atomic claim)
SpotAllocator
—
Lock granularity trade-off — per-spot CAS is finer/faster than per-floor mutex
W. Concurrency
Shared mutable state: the pool of available spots (per floor, per size). Two threads calling allocate() concurrently for the last spot of a size is the canonical race.
Critical section: "check spot is free" + "mark it occupied" must be a single atomic step — never two separate steps a thread could be pre-empted between.
Pessimistic approach: a mutex per floor (or per size-bucket) guarding the check-and-occupy. Simple, safe, some contention under heavy concurrent entry.
Optimistic approach: model spot state as an atomic (e.g. std::atomic<bool> or a CAS-based state field) and use compare-and-swap to claim a spot — no lock held, but requires careful retry logic if CAS fails.
Lock granularity trade-off: one global lock on the whole lot is simplest but serializes all entries lot-wide (bad under load); per-spot atomics give maximum parallelism but more code complexity.
Idempotency: if a client retries a park request after a timeout (not knowing if it succeeded), the system must not double-allocate — this argues for the caller supplying an idempotency key, or the atomic claim being the only side effect that matters.
Deadlock avoidance: if a design ever needs to lock two spots at once (rare here — parking doesn't naturally need it), always acquire locks in a consistent global order (e.g., by spot ID) to avoid circular wait.
X. Persistence
If the lot must survive a restart: ParkingSpot, Floor, and Ticket become persistent entities with durable IDs (not just in-memory object references). A TicketRepository/SpotRepository abstraction replaces direct in-memory maps — same interface shape, different backing store. Occupying a spot becomes a transaction (or an optimistic-locking update with a version column) rather than a plain in-memory mutation, so two processes can't both "win" the same spot across a crash/restart boundary. Ticket IDs should be generated in a way that's safe across restarts (UUID, not an in-memory counter).
Y. Scaling Boundary
Layer
What Belongs Here
LLD (this document)
Object relationships, responsibilities, APIs, in-process state, patterns for one lot
HLD
A ParkingService behind a load balancer, a database per lot or shared with lot_id partitioning, a cache for "current availability" reads
Distributed Systems
Only relevant if managing thousands of lots city-wide: partitioning by lot ID, eventual consistency for availability displays, distributed locks only if two data centers could race on the same physical spot (unlikely — a spot belongs to exactly one lot, so this rarely needs true distributed locking)
Z. Failure Scenarios
Duplicate exit request: ticket already CLOSED — second unparkVehicle call must be rejected, not double-charge.
Timeout mid-park: client doesn't get the ticket response but the spot was actually claimed — client retry must not double-allocate (idempotency key or "check if this plate already has an active ticket" guard).
Crash between "spot occupied" and "ticket created": reconciliation on restart should treat "occupied but no matching ticket" as an anomaly to flag, not silently trust.
External payment failure at exit: spot should not be released until payment is confirmed (or a grace/retry policy is defined) — otherwise a car could be double-billed or the spot double-allocated while the driver is still physically parked.
AA. Common Bad Designs
One giant ParkingLot god class doing allocation + pricing + payment + ticket printing. Looks reasonable early (fewer files!) but every new requirement means editing the same massive class — violates SRP and OCP simultaneously. Fix: extract SpotAllocator and FeeStrategy as soon as either shows signs of variability.
Singleton everywhere (ParkingLot, SpotAllocator, FeeStrategy all singletons). Looks like "there's only one of these in production" — but makes unit testing (need 2 lots side by side) and swapping strategies at runtime awkward. Fix: single instances via dependency injection at the application-wiring layer, not baked into the class itself.
Strategy pattern for every tiny behavior (a strategy interface for "how to format a ticket ID," for instance). Looks thorough, but adds indirection with no variability to justify it. Fix: only introduce a Strategy when there's a real, stated reason it needs to vary.
Type-string switch statements (if vehicle.type == "car") instead of polymorphism. Looks quick to write, but every new vehicle type means hunting down every switch statement across the codebase. Fix: polymorphic getSize()/behavior on the Vehicle hierarchy itself.
AB. Design Trade-Offs
Decision
Option A
Option B
Trade-off
Vehicle-spot matching
Exact type-to-type map
Size-based hierarchy
Type map is simpler to explain but doesn't generalize; size-based scales to new types for free but requires agreeing on an ordering
Lock granularity
One lock per lot
One lock/atomic per spot
Coarse lock = simple, more contention; fine-grained = more parallel, more complex correctness reasoning
Allocation policy
First-available
Nearest-to-entrance (heap-based)
First-available is O(1) and simple; nearest-spot is better UX but O(log n) and more state (one heap per entrance)
Map vs. list for spot pool
Map<Size, List<Spot>>
Flat List<Spot> + linear scan
Map is faster at scale; flat list is simpler to reason about for a small lot and easier to unit test
AC. Interviewer Evaluation
Weak candidate: jumps straight to code; one big class; hardcodes vehicle types with if/else; never asks a clarifying question.
Average candidate: asks 1–2 clarifying questions; produces a reasonable class split (Vehicle, Spot, Ticket, Lot) but pricing/allocation logic is still inline inside ParkingLot.
Strong candidate: asks clarifying questions up front; extracts FeeStrategy/SpotAllocator proactively because they identify pricing/allocation as "likely to change"; discusses trade-offs verbally without prompting.
Exceptional candidate: does all of the above, then anticipates the concurrency follow-up before being asked ("I'll note that allocate-and-occupy needs to be atomic once we have multiple entrances"), and can articulate exactly where the LLD boundary ends and HLD begins if pushed on scale.
AD. Interview Simulation
Interviewer: Design a parking lot system.
Candidate: Before I start — what vehicle types and spot types should I support, and can those grow over time?
Interviewer: Assume motorcycles, cars, and buses, and yes, more types could be added later.
Candidate: Then I'll model matching by a VehicleSize concept rather than exact type-to-type pairs, so new vehicle types don't require new spot logic.
Interviewer: Good. Assume it's multi-floor.
Candidate: I'll introduce a Floor that composes ParkingSpots, sitting between the top-level ParkingLot and individual spots.
Candidate: I'll isolate that behind a FeeStrategy interface so ParkingLot never needs to change when pricing rules change — that's Open/Closed in practice, not just a buzzword.
Interviewer: Now, two cars arrive at the exact same time for the last available compact spot.
Candidate: The allocate-and-occupy step has to be atomic. I'd guard it with a lock scoped to the spot pool being checked — or use an atomic compare-and-swap on the spot's state if I want to avoid blocking. I'd pick the lock first for clarity, and mention the CAS alternative if pushed on throughput.
Interviewer: What if the payment provider fails after the car has already left?
Candidate: That's a case for not releasing the spot until payment is confirmed, plus making the fee-charge operation idempotent so a retry after a timeout doesn't double-charge.
Interviewer: Last one — how would this look if we had to support a thousand lots citywide?
Candidate: At that point it crosses from LLD into HLD — I'd keep this exact object model per lot, but partition storage by lot ID and put a service layer with a database and cache in front of it. The class design here doesn't need to change; only where it runs does.
A complete, compilable reference implementation of every class in the diagram above (composition, inheritance, interface realization, and dependency-injected strategies all preserved per section I), with a runnable demo exercising the primary flow — in Java, C++, JavaScript, and TypeScript.
// Parking Lot - full LLD implementation (single-file demo).
// Difficulty badge includes "Concurrency", so spot allocation + occupation is guarded
// by a lock to guarantee two concurrent parkVehicle() calls never get the same spot.
import java.util.*;
import java.util.concurrent.locks.*;
public class p1 {
// ---------- Vehicle hierarchy ----------
enum VehicleSize { MOTORCYCLE, COMPACT, LARGE }
static abstract class Vehicle {
private final String licensePlate;
Vehicle(String licensePlate) { this.licensePlate = licensePlate; }
String getLicensePlate() { return licensePlate; }
abstract VehicleSize getSize();
}
static class Motorcycle extends Vehicle {
Motorcycle(String plate) { super(plate); }
VehicleSize getSize() { return VehicleSize.MOTORCYCLE; }
}
static class Car extends Vehicle {
Car(String plate) { super(plate); }
VehicleSize getSize() { return VehicleSize.COMPACT; }
}
static class Bus extends Vehicle {
Bus(String plate) { super(plate); }
VehicleSize getSize() { return VehicleSize.LARGE; }
}
// ---------- ParkingSpot ----------
static class ParkingSpot {
private final String id;
private final VehicleSize size;
private boolean occupied = false;
private Vehicle currentVehicle;
ParkingSpot(String id, VehicleSize size) { this.id = id; this.size = size; }
String getId() { return id; }
VehicleSize getSize() { return size; }
boolean isOccupied() { return occupied; }
// A larger spot can host a smaller vehicle; ordinal() works because the enum
// is declared smallest-to-largest.
boolean canFit(VehicleSize vehicleSize) { return !occupied && size.ordinal() >= vehicleSize.ordinal(); }
void occupy(Vehicle vehicle) {
if (occupied) throw new IllegalStateException("Spot " + id + " already occupied");
occupied = true;
currentVehicle = vehicle;
}
void vacate() {
occupied = false;
currentVehicle = null;
}
}
// ---------- Floor ----------
static class Floor {
private final int floorNumber;
private final List<ParkingSpot> spots;
Floor(int floorNumber, List<ParkingSpot> spots) {
this.floorNumber = floorNumber;
this.spots = spots;
}
int getFloorNumber() { return floorNumber; }
List<ParkingSpot> getSpots() { return spots; }
// First compatible free spot on this floor, or null if none.
ParkingSpot findAvailableSpot(VehicleSize size) {
for (ParkingSpot s : spots) {
if (s.canFit(size)) return s;
}
return null;
}
}
// ---------- SpotAllocator (interface) + strategies ----------
interface SpotAllocator {
ParkingSpot allocate(Vehicle vehicle);
}
// Picks the first free compatible spot, scanning floors in order.
static class FirstAvailableAllocator implements SpotAllocator {
private final List<Floor> floors;
FirstAvailableAllocator(List<Floor> floors) { this.floors = floors; }
public ParkingSpot allocate(Vehicle vehicle) {
for (Floor f : floors) {
ParkingSpot spot = f.findAvailableSpot(vehicle.getSize());
if (spot != null) return spot;
}
return null;
}
}
// Picks the compatible spot whose size is the closest fit (least wasted space),
// scanning floors in order and comparing candidates across floors.
static class NearestSpotAllocator implements SpotAllocator {
private final List<Floor> floors;
NearestSpotAllocator(List<Floor> floors) { this.floors = floors; }
public ParkingSpot allocate(Vehicle vehicle) {
ParkingSpot best = null;
for (Floor f : floors) {
for (ParkingSpot s : f.getSpots()) {
if (!s.canFit(vehicle.getSize())) continue;
if (best == null || s.getSize().ordinal() < best.getSize().ordinal()) best = s;
}
}
return best;
}
}
// ---------- Ticket ----------
enum TicketStatus { ACTIVE, CLOSED }
static class Ticket {
private static int SEQ = 1;
private final String id;
private final Vehicle vehicle;
private final ParkingSpot spot;
private final long entryTime;
private long exitTime;
private TicketStatus status;
private Ticket(Vehicle vehicle, ParkingSpot spot, long entryTime) {
this.id = "T-" + (SEQ++);
this.vehicle = vehicle;
this.spot = spot;
this.entryTime = entryTime;
this.status = TicketStatus.ACTIVE;
}
static Ticket create(Vehicle vehicle, ParkingSpot spot, long entryTime) {
return new Ticket(vehicle, spot, entryTime);
}
void close(long exitTime) {
this.exitTime = exitTime;
this.status = TicketStatus.CLOSED;
}
String getId() { return id; }
Vehicle getVehicle() { return vehicle; }
ParkingSpot getSpot() { return spot; }
long getEntryTime() { return entryTime; }
long getExitTime() { return exitTime; }
TicketStatus getStatus() { return status; }
}
// ---------- FeeStrategy (interface) + strategies ----------
interface FeeStrategy {
double calculate(Ticket closedTicket);
}
static class HourlyFeeStrategy implements FeeStrategy {
private final double ratePerHour;
HourlyFeeStrategy(double ratePerHour) { this.ratePerHour = ratePerHour; }
public double calculate(Ticket t) {
long durationMs = t.getExitTime() - t.getEntryTime();
double hours = Math.max(1, Math.ceil(durationMs / 3600000.0)); // round up, min 1 hour
return hours * ratePerHour;
}
}
// Applies a surcharge multiplier if entry happened during peak hours (9-11, 17-19).
static class PeakHourFeeStrategy implements FeeStrategy {
private final double ratePerHour;
private final double peakMultiplier;
PeakHourFeeStrategy(double ratePerHour, double peakMultiplier) {
this.ratePerHour = ratePerHour;
this.peakMultiplier = peakMultiplier;
}
public double calculate(Ticket t) {
long durationMs = t.getExitTime() - t.getEntryTime();
double hours = Math.max(1, Math.ceil(durationMs / 3600000.0));
int entryHour = (int) ((t.getEntryTime() / 3600000) % 24);
boolean peak = (entryHour >= 9 && entryHour < 11) || (entryHour >= 17 && entryHour < 19);
return hours * ratePerHour * (peak ? peakMultiplier : 1.0);
}
}
// ---------- Exceptions ----------
static class NoAvailableSpotException extends RuntimeException {
NoAvailableSpotException(String msg) { super(msg); }
}
static class InvalidTicketException extends RuntimeException {
InvalidTicketException(String msg) { super(msg); }
}
// ---------- ParkingLot (facade) ----------
static class ParkingLot {
private final List<Floor> floors;
private final SpotAllocator allocator; // injected interface, not a concrete class
private final FeeStrategy feeStrategy; // injected interface, not a concrete class
// Single lock guarding the "decide spot + occupy spot" sequence, which spans two
// separate objects (allocator, spot) and therefore cannot be made atomic by either
// object alone.
private final Lock lock = new ReentrantLock();
ParkingLot(List<Floor> floors, SpotAllocator allocator, FeeStrategy feeStrategy) {
this.floors = floors;
this.allocator = allocator;
this.feeStrategy = feeStrategy;
}
Ticket parkVehicle(Vehicle vehicle) {
lock.lock();
try {
ParkingSpot spot = allocator.allocate(vehicle);
if (spot == null) throw new NoAvailableSpotException("No available spot for " + vehicle.getSize());
spot.occupy(vehicle);
return Ticket.create(vehicle, spot, System.currentTimeMillis());
} finally {
lock.unlock();
}
}
double unparkVehicle(Ticket ticket) {
lock.lock();
try {
if (ticket.getStatus() != TicketStatus.ACTIVE) throw new InvalidTicketException("Ticket not active: " + ticket.getId());
ticket.close(System.currentTimeMillis());
double fee = feeStrategy.calculate(ticket);
ticket.getSpot().vacate();
return fee;
} finally {
lock.unlock();
}
}
}
// ---------- Demo ----------
public static void main(String[] args) {
List<ParkingSpot> f1Spots = Arrays.asList(
new ParkingSpot("F1-M1", VehicleSize.MOTORCYCLE),
new ParkingSpot("F1-C1", VehicleSize.COMPACT)
);
Floor floor1 = new Floor(1, f1Spots);
List<Floor> floors = new ArrayList<>(Collections.singletonList(floor1));
SpotAllocator allocator = new NearestSpotAllocator(floors);
FeeStrategy feeStrategy = new HourlyFeeStrategy(10.0);
ParkingLot lot = new ParkingLot(floors, allocator, feeStrategy);
Car car = new Car("KA-01-1234");
Ticket t1 = lot.parkVehicle(car);
System.out.println("Parked car, ticket=" + t1.getId() + " spot=" + t1.getSpot().getId());
Motorcycle bike = new Motorcycle("KA-01-5678");
Ticket t2 = lot.parkVehicle(bike);
System.out.println("Parked bike, ticket=" + t2.getId() + " spot=" + t2.getSpot().getId());
try {
Bus bus = new Bus("KA-01-9999");
lot.parkVehicle(bus); // no LARGE spot exists -> should fail
} catch (NoAvailableSpotException e) {
System.out.println("Expected failure parking bus: " + e.getMessage());
}
double fee = lot.unparkVehicle(t1);
System.out.println("Unparked car, fee=" + fee);
}
}
// Parking Lot - full LLD implementation (single-file demo).
// Difficulty badge includes "Concurrency", so spot allocation + occupation is guarded
// by a mutex to guarantee two concurrent parkVehicle() calls never get the same spot.
#include <iostream>
#include <vector>
#include <memory>
#include <string>
#include <mutex>
#include <stdexcept>
#include <chrono>
#include <cmath>
// ---------- Vehicle hierarchy ----------
enum class VehicleSize { MOTORCYCLE = 0, COMPACT = 1, LARGE = 2 };
class Vehicle {
public:
Vehicle(std::string plate) : licensePlate(std::move(plate)) {}
virtual ~Vehicle() = default;
const std::string& getLicensePlate() const { return licensePlate; }
virtual VehicleSize getSize() const = 0;
private:
std::string licensePlate;
};
class Motorcycle : public Vehicle {
public:
explicit Motorcycle(std::string plate) : Vehicle(std::move(plate)) {}
VehicleSize getSize() const override { return VehicleSize::MOTORCYCLE; }
};
class Car : public Vehicle {
public:
explicit Car(std::string plate) : Vehicle(std::move(plate)) {}
VehicleSize getSize() const override { return VehicleSize::COMPACT; }
};
class Bus : public Vehicle {
public:
explicit Bus(std::string plate) : Vehicle(std::move(plate)) {}
VehicleSize getSize() const override { return VehicleSize::LARGE; }
};
// ---------- ParkingSpot ----------
class ParkingSpot {
public:
ParkingSpot(std::string id, VehicleSize size) : id(std::move(id)), size(size) {}
const std::string& getId() const { return id; }
VehicleSize getSize() const { return size; }
bool isOccupied() const { return occupied; }
// A larger spot can host a smaller vehicle; enum values are ordered smallest-to-largest.
bool canFit(VehicleSize vehicleSize) const {
return !occupied && static_cast<int>(size) >= static_cast<int>(vehicleSize);
}
void occupy(Vehicle* vehicle) {
if (occupied) throw std::runtime_error("Spot " + id + " already occupied");
occupied = true;
currentVehicle = vehicle;
}
void vacate() {
occupied = false;
currentVehicle = nullptr;
}
private:
std::string id;
VehicleSize size;
bool occupied = false;
Vehicle* currentVehicle = nullptr;
};
// ---------- Floor ----------
class Floor {
public:
Floor(int floorNumber, std::vector<std::unique_ptr<ParkingSpot>> spots)
: floorNumber(floorNumber), spots(std::move(spots)) {}
int getFloorNumber() const { return floorNumber; }
std::vector<std::unique_ptr<ParkingSpot>>& getSpots() { return spots; }
// First compatible free spot on this floor, or nullptr if none.
ParkingSpot* findAvailableSpot(VehicleSize size) {
for (auto& s : spots) {
if (s->canFit(size)) return s.get();
}
return nullptr;
}
private:
int floorNumber;
std::vector<std::unique_ptr<ParkingSpot>> spots;
};
// ---------- Ticket ----------
enum class TicketStatus { ACTIVE, CLOSED };
class Ticket {
public:
static std::unique_ptr<Ticket> create(Vehicle* vehicle, ParkingSpot* spot, long long entryTime) {
return std::unique_ptr<Ticket>(new Ticket(vehicle, spot, entryTime));
}
void close(long long exit) { exitTime = exit; status = TicketStatus::CLOSED; }
const std::string& getId() const { return id; }
Vehicle* getVehicle() const { return vehicle; }
ParkingSpot* getSpot() const { return spot; }
long long getEntryTime() const { return entryTime; }
long long getExitTime() const { return exitTime; }
TicketStatus getStatus() const { return status; }
private:
Ticket(Vehicle* vehicle, ParkingSpot* spot, long long entryTime)
: id("T-" + std::to_string(nextId++)), vehicle(vehicle), spot(spot),
entryTime(entryTime), exitTime(0), status(TicketStatus::ACTIVE) {}
static int nextId;
std::string id;
Vehicle* vehicle;
ParkingSpot* spot;
long long entryTime;
long long exitTime;
TicketStatus status;
};
int Ticket::nextId = 1;
// ---------- SpotAllocator (interface) + strategies ----------
class SpotAllocator {
public:
virtual ~SpotAllocator() = default;
virtual ParkingSpot* allocate(Vehicle* vehicle) = 0;
};
// Picks the first free compatible spot, scanning floors in order.
class FirstAvailableAllocator : public SpotAllocator {
public:
explicit FirstAvailableAllocator(std::vector<Floor*> floors) : floors(std::move(floors)) {}
ParkingSpot* allocate(Vehicle* vehicle) override {
for (auto* f : floors) {
ParkingSpot* spot = f->findAvailableSpot(vehicle->getSize());
if (spot) return spot;
}
return nullptr;
}
private:
std::vector<Floor*> floors;
};
// Picks the compatible spot whose size is the closest fit (least wasted space).
class NearestSpotAllocator : public SpotAllocator {
public:
explicit NearestSpotAllocator(std::vector<Floor*> floors) : floors(std::move(floors)) {}
ParkingSpot* allocate(Vehicle* vehicle) override {
ParkingSpot* best = nullptr;
for (auto* f : floors) {
for (auto& s : f->getSpots()) {
if (!s->canFit(vehicle->getSize())) continue;
if (!best || static_cast<int>(s->getSize()) < static_cast<int>(best->getSize())) best = s.get();
}
}
return best;
}
private:
std::vector<Floor*> floors;
};
// ---------- FeeStrategy (interface) + strategies ----------
class FeeStrategy {
public:
virtual ~FeeStrategy() = default;
virtual double calculate(const Ticket& closedTicket) const = 0;
};
class HourlyFeeStrategy : public FeeStrategy {
public:
explicit HourlyFeeStrategy(double ratePerHour) : ratePerHour(ratePerHour) {}
double calculate(const Ticket& t) const override {
long long durationMs = t.getExitTime() - t.getEntryTime();
double hours = std::max(1.0, std::ceil(durationMs / 3600000.0));
return hours * ratePerHour;
}
private:
double ratePerHour;
};
// Applies a surcharge multiplier if entry happened during peak hours (9-11, 17-19).
class PeakHourFeeStrategy : public FeeStrategy {
public:
PeakHourFeeStrategy(double ratePerHour, double peakMultiplier)
: ratePerHour(ratePerHour), peakMultiplier(peakMultiplier) {}
double calculate(const Ticket& t) const override {
long long durationMs = t.getExitTime() - t.getEntryTime();
double hours = std::max(1.0, std::ceil(durationMs / 3600000.0));
int entryHour = static_cast<int>((t.getEntryTime() / 3600000) % 24);
bool peak = (entryHour >= 9 && entryHour < 11) || (entryHour >= 17 && entryHour < 19);
return hours * ratePerHour * (peak ? peakMultiplier : 1.0);
}
private:
double ratePerHour;
double peakMultiplier;
};
// ---------- Exceptions ----------
class NoAvailableSpotException : public std::runtime_error {
public:
explicit NoAvailableSpotException(const std::string& msg) : std::runtime_error(msg) {}
};
class InvalidTicketException : public std::runtime_error {
public:
explicit InvalidTicketException(const std::string& msg) : std::runtime_error(msg) {}
};
// ---------- ParkingLot (facade) ----------
class ParkingLot {
public:
ParkingLot(std::vector<Floor*> floors, std::unique_ptr<SpotAllocator> allocator,
std::unique_ptr<FeeStrategy> feeStrategy)
: floors(std::move(floors)), allocator(std::move(allocator)), feeStrategy(std::move(feeStrategy)) {}
std::unique_ptr<Ticket> parkVehicle(Vehicle* vehicle) {
// Single lock guarding "decide spot + occupy spot", which spans two separate
// objects (allocator, spot) and cannot be made atomic by either object alone.
std::lock_guard<std::mutex> guard(lock);
ParkingSpot* spot = allocator->allocate(vehicle);
if (!spot) throw NoAvailableSpotException("No available spot for this vehicle size");
spot->occupy(vehicle);
return Ticket::create(vehicle, spot, nowMs());
}
double unparkVehicle(Ticket* ticket) {
std::lock_guard<std::mutex> guard(lock);
if (ticket->getStatus() != TicketStatus::ACTIVE) throw InvalidTicketException("Ticket not active: " + ticket->getId());
ticket->close(nowMs());
double fee = feeStrategy->calculate(*ticket);
ticket->getSpot()->vacate();
return fee;
}
private:
static long long nowMs() {
return std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch())
.count();
}
std::vector<Floor*> floors;
std::unique_ptr<SpotAllocator> allocator;
std::unique_ptr<FeeStrategy> feeStrategy;
std::mutex lock;
};
// ---------- Demo ----------
int main() {
std::vector<std::unique_ptr<ParkingSpot>> f1Spots;
f1Spots.push_back(std::make_unique<ParkingSpot>("F1-M1", VehicleSize::MOTORCYCLE));
f1Spots.push_back(std::make_unique<ParkingSpot>("F1-C1", VehicleSize::COMPACT));
Floor floor1(1, std::move(f1Spots));
std::vector<Floor*> floors = {&floor1};
auto allocator = std::make_unique<NearestSpotAllocator>(floors);
auto feeStrategy = std::make_unique<HourlyFeeStrategy>(10.0);
ParkingLot lot(floors, std::move(allocator), std::move(feeStrategy));
Car car("KA-01-1234");
auto t1 = lot.parkVehicle(&car);
std::cout << "Parked car, ticket=" << t1->getId() << " spot=" << t1->getSpot()->getId() << "\n";
Motorcycle bike("KA-01-5678");
auto t2 = lot.parkVehicle(&bike);
std::cout << "Parked bike, ticket=" << t2->getId() << " spot=" << t2->getSpot()->getId() << "\n";
try {
Bus bus("KA-01-9999");
lot.parkVehicle(&bus); // no LARGE spot exists -> should fail
} catch (const NoAvailableSpotException& e) {
std::cout << "Expected failure parking bus: " << e.what() << "\n";
}
double fee = lot.unparkVehicle(t1.get());
std::cout << "Unparked car, fee=" << fee << "\n";
return 0;
}
// Parking Lot - full LLD implementation (single-file demo).
// Difficulty badge includes "Concurrency". Node's event loop is single-threaded, so true
// data races can't happen here, but we still model the critical section explicitly with a
// tiny async mutex so the *design* correctly reflects "allocate+occupy must be atomic" -
// this matters if this class were ever ported to a worker-thread / multi-process context.
// ---------- Vehicle hierarchy ----------
const VehicleSize = { MOTORCYCLE: 0, COMPACT: 1, LARGE: 2 };
class Vehicle {
constructor(licensePlate) {
if (new.target === Vehicle) throw new Error("Vehicle is abstract");
this.licensePlate = licensePlate;
}
getSize() {
throw new Error("getSize() must be implemented by subclass");
}
}
class Motorcycle extends Vehicle {
getSize() { return VehicleSize.MOTORCYCLE; }
}
class Car extends Vehicle {
getSize() { return VehicleSize.COMPACT; }
}
class Bus extends Vehicle {
getSize() { return VehicleSize.LARGE; }
}
// ---------- ParkingSpot ----------
class ParkingSpot {
constructor(id, size) {
this.id = id;
this.size = size;
this.occupied = false;
this.currentVehicle = null;
}
// A larger spot can host a smaller vehicle; VehicleSize values are ordered
// smallest-to-largest so a simple numeric compare works.
canFit(vehicleSize) {
return !this.occupied && this.size >= vehicleSize;
}
occupy(vehicle) {
if (this.occupied) throw new Error(`Spot ${this.id} already occupied`);
this.occupied = true;
this.currentVehicle = vehicle;
}
vacate() {
this.occupied = false;
this.currentVehicle = null;
}
}
// ---------- Floor ----------
class Floor {
constructor(floorNumber, spots) {
this.floorNumber = floorNumber;
this.spots = spots;
}
// First compatible free spot on this floor, or null if none.
findAvailableSpot(size) {
for (const s of this.spots) {
if (s.canFit(size)) return s;
}
return null;
}
}
// ---------- Ticket ----------
const TicketStatus = { ACTIVE: "ACTIVE", CLOSED: "CLOSED" };
let __ticketSeq = 1;
class Ticket {
constructor(vehicle, spot, entryTime) {
this.id = `T-${__ticketSeq++}`;
this.vehicle = vehicle;
this.spot = spot;
this.entryTime = entryTime;
this.exitTime = null;
this.status = TicketStatus.ACTIVE;
}
static create(vehicle, spot, entryTime) {
return new Ticket(vehicle, spot, entryTime);
}
close(exitTime) {
this.exitTime = exitTime;
this.status = TicketStatus.CLOSED;
}
}
// ---------- SpotAllocator (interface) + strategies ----------
class SpotAllocator {
allocate(_vehicle) {
throw new Error("allocate() must be implemented");
}
}
// Picks the first free compatible spot, scanning floors in order.
class FirstAvailableAllocator extends SpotAllocator {
constructor(floors) {
super();
this.floors = floors;
}
allocate(vehicle) {
for (const f of this.floors) {
const spot = f.findAvailableSpot(vehicle.getSize());
if (spot) return spot;
}
return null;
}
}
// Picks the compatible spot whose size is the closest fit (least wasted space).
class NearestSpotAllocator extends SpotAllocator {
constructor(floors) {
super();
this.floors = floors;
}
allocate(vehicle) {
let best = null;
for (const f of this.floors) {
for (const s of f.spots) {
if (!s.canFit(vehicle.getSize())) continue;
if (!best || s.size < best.size) best = s;
}
}
return best;
}
}
// ---------- FeeStrategy (interface) + strategies ----------
class FeeStrategy {
calculate(_closedTicket) {
throw new Error("calculate() must be implemented");
}
}
class HourlyFeeStrategy extends FeeStrategy {
constructor(ratePerHour) {
super();
this.ratePerHour = ratePerHour;
}
calculate(t) {
const durationMs = t.exitTime - t.entryTime;
const hours = Math.max(1, Math.ceil(durationMs / 3600000));
return hours * this.ratePerHour;
}
}
// Applies a surcharge multiplier if entry happened during peak hours (9-11, 17-19).
class PeakHourFeeStrategy extends FeeStrategy {
constructor(ratePerHour, peakMultiplier) {
super();
this.ratePerHour = ratePerHour;
this.peakMultiplier = peakMultiplier;
}
calculate(t) {
const durationMs = t.exitTime - t.entryTime;
const hours = Math.max(1, Math.ceil(durationMs / 3600000));
const entryHour = Math.floor(t.entryTime / 3600000) % 24;
const peak = (entryHour >= 9 && entryHour < 11) || (entryHour >= 17 && entryHour < 19);
return hours * this.ratePerHour * (peak ? this.peakMultiplier : 1);
}
}
// ---------- Errors ----------
class NoAvailableSpotException extends Error {}
class InvalidTicketException extends Error {}
// ---------- Tiny async mutex (see file header for why) ----------
class Mutex {
constructor() { this._locked = false; this._waiters = []; }
async acquire() {
if (!this._locked) { this._locked = true; return; }
await new Promise((resolve) => this._waiters.push(resolve));
this._locked = true;
}
release() {
this._locked = false;
const next = this._waiters.shift();
if (next) next();
}
}
// ---------- ParkingLot (facade) ----------
class ParkingLot {
constructor(floors, allocator, feeStrategy) {
this.floors = floors;
this.allocator = allocator; // injected interface, not a concrete class
this.feeStrategy = feeStrategy; // injected interface, not a concrete class
this.lock = new Mutex();
}
async parkVehicle(vehicle) {
await this.lock.acquire();
try {
const spot = this.allocator.allocate(vehicle);
if (!spot) throw new NoAvailableSpotException(`No available spot for vehicle size ${vehicle.getSize()}`);
spot.occupy(vehicle);
return Ticket.create(vehicle, spot, Date.now());
} finally {
this.lock.release();
}
}
async unparkVehicle(ticket) {
await this.lock.acquire();
try {
if (ticket.status !== TicketStatus.ACTIVE) throw new InvalidTicketException(`Ticket not active: ${ticket.id}`);
ticket.close(Date.now());
const fee = this.feeStrategy.calculate(ticket);
ticket.spot.vacate();
return fee;
} finally {
this.lock.release();
}
}
}
// ---------- Demo ----------
async function main() {
const f1Spots = [
new ParkingSpot("F1-M1", VehicleSize.MOTORCYCLE),
new ParkingSpot("F1-C1", VehicleSize.COMPACT),
];
const floor1 = new Floor(1, f1Spots);
const floors = [floor1];
const allocator = new NearestSpotAllocator(floors);
const feeStrategy = new HourlyFeeStrategy(10.0);
const lot = new ParkingLot(floors, allocator, feeStrategy);
const car = new Car("KA-01-1234");
const t1 = await lot.parkVehicle(car);
console.log(`Parked car, ticket=${t1.id} spot=${t1.spot.id}`);
const bike = new Motorcycle("KA-01-5678");
const t2 = await lot.parkVehicle(bike);
console.log(`Parked bike, ticket=${t2.id} spot=${t2.spot.id}`);
try {
const bus = new Bus("KA-01-9999");
await lot.parkVehicle(bus); // no LARGE spot exists -> should fail
} catch (e) {
console.log(`Expected failure parking bus: ${e.message}`);
}
const fee = await lot.unparkVehicle(t1);
console.log(`Unparked car, fee=${fee}`);
}
if (require.main === module) {
main();
}
module.exports = {
VehicleSize, Vehicle, Motorcycle, Car, Bus, ParkingSpot, Floor, Ticket, TicketStatus,
SpotAllocator, FirstAvailableAllocator, NearestSpotAllocator,
FeeStrategy, HourlyFeeStrategy, PeakHourFeeStrategy,
NoAvailableSpotException, InvalidTicketException, ParkingLot,
};
// Parking Lot - full LLD implementation (single-file demo).
// Difficulty badge includes "Concurrency". JS/TS is single-threaded, so true data races
// can't happen, but the critical section is still modeled explicitly with a tiny async
// mutex so the *design* reflects "allocate+occupy must be atomic" faithfully.
// ---------- Vehicle hierarchy ----------
enum VehicleSize {
MOTORCYCLE = 0,
COMPACT = 1,
LARGE = 2,
}
abstract class Vehicle {
constructor(protected readonly licensePlate: string) {}
abstract getSize(): VehicleSize;
getLicensePlate(): string { return this.licensePlate; }
}
class Motorcycle extends Vehicle {
getSize(): VehicleSize { return VehicleSize.MOTORCYCLE; }
}
class Car extends Vehicle {
getSize(): VehicleSize { return VehicleSize.COMPACT; }
}
class Bus extends Vehicle {
getSize(): VehicleSize { return VehicleSize.LARGE; }
}
// ---------- ParkingSpot ----------
class ParkingSpot {
private occupied = false;
private currentVehicle: Vehicle | null = null;
constructor(private readonly id: string, private readonly size: VehicleSize) {}
getId(): string { return this.id; }
getSize(): VehicleSize { return this.size; }
isOccupied(): boolean { return this.occupied; }
// A larger spot can host a smaller vehicle; VehicleSize is ordered smallest-to-largest.
canFit(vehicleSize: VehicleSize): boolean {
return !this.occupied && this.size >= vehicleSize;
}
occupy(vehicle: Vehicle): void {
if (this.occupied) throw new Error(`Spot ${this.id} already occupied`);
this.occupied = true;
this.currentVehicle = vehicle;
}
vacate(): void {
this.occupied = false;
this.currentVehicle = null;
}
}
// ---------- Floor ----------
class Floor {
constructor(private readonly floorNumber: number, private readonly spots: ParkingSpot[]) {}
getFloorNumber(): number { return this.floorNumber; }
getSpots(): ParkingSpot[] { return this.spots; }
// First compatible free spot on this floor, or null if none.
findAvailableSpot(size: VehicleSize): ParkingSpot | null {
for (const s of this.spots) {
if (s.canFit(size)) return s;
}
return null;
}
}
// ---------- Ticket ----------
enum TicketStatus { ACTIVE = "ACTIVE", CLOSED = "CLOSED" }
let ticketSeq = 1;
class Ticket {
readonly id: string;
exitTime: number | null = null;
status: TicketStatus = TicketStatus.ACTIVE;
private constructor(
readonly vehicle: Vehicle,
readonly spot: ParkingSpot,
readonly entryTime: number
) {
this.id = `T-${ticketSeq++}`;
}
static create(vehicle: Vehicle, spot: ParkingSpot, entryTime: number): Ticket {
return new Ticket(vehicle, spot, entryTime);
}
close(exitTime: number): void {
this.exitTime = exitTime;
this.status = TicketStatus.CLOSED;
}
}
// ---------- SpotAllocator (interface) + strategies ----------
interface SpotAllocator {
allocate(vehicle: Vehicle): ParkingSpot | null;
}
// Picks the first free compatible spot, scanning floors in order.
class FirstAvailableAllocator implements SpotAllocator {
constructor(private readonly floors: Floor[]) {}
allocate(vehicle: Vehicle): ParkingSpot | null {
for (const f of this.floors) {
const spot = f.findAvailableSpot(vehicle.getSize());
if (spot) return spot;
}
return null;
}
}
// Picks the compatible spot whose size is the closest fit (least wasted space).
class NearestSpotAllocator implements SpotAllocator {
constructor(private readonly floors: Floor[]) {}
allocate(vehicle: Vehicle): ParkingSpot | null {
let best: ParkingSpot | null = null;
for (const f of this.floors) {
for (const s of f.getSpots()) {
if (!s.canFit(vehicle.getSize())) continue;
if (!best || s.getSize() < best.getSize()) best = s;
}
}
return best;
}
}
// ---------- FeeStrategy (interface) + strategies ----------
interface FeeStrategy {
calculate(closedTicket: Ticket): number;
}
class HourlyFeeStrategy implements FeeStrategy {
constructor(private readonly ratePerHour: number) {}
calculate(t: Ticket): number {
const durationMs = (t.exitTime ?? 0) - t.entryTime;
const hours = Math.max(1, Math.ceil(durationMs / 3600000));
return hours * this.ratePerHour;
}
}
// Applies a surcharge multiplier if entry happened during peak hours (9-11, 17-19).
class PeakHourFeeStrategy implements FeeStrategy {
constructor(private readonly ratePerHour: number, private readonly peakMultiplier: number) {}
calculate(t: Ticket): number {
const durationMs = (t.exitTime ?? 0) - t.entryTime;
const hours = Math.max(1, Math.ceil(durationMs / 3600000));
const entryHour = Math.floor(t.entryTime / 3600000) % 24;
const peak = (entryHour >= 9 && entryHour < 11) || (entryHour >= 17 && entryHour < 19);
return hours * this.ratePerHour * (peak ? this.peakMultiplier : 1);
}
}
// ---------- Errors ----------
class NoAvailableSpotException extends Error {}
class InvalidTicketException extends Error {}
// ---------- Tiny async mutex (see file header for why) ----------
class Mutex {
private locked = false;
private waiters: Array<() => void> = [];
async acquire(): Promise<void> {
if (!this.locked) { this.locked = true; return; }
await new Promise<void>((resolve) => this.waiters.push(resolve));
this.locked = true;
}
release(): void {
this.locked = false;
const next = this.waiters.shift();
if (next) next();
}
}
// ---------- ParkingLot (facade) ----------
class ParkingLot {
private readonly lock = new Mutex();
constructor(
private readonly floors: Floor[],
private readonly allocator: SpotAllocator, // injected interface, not a concrete class
private readonly feeStrategy: FeeStrategy // injected interface, not a concrete class
) {}
async parkVehicle(vehicle: Vehicle): Promise<Ticket> {
await this.lock.acquire();
try {
const spot = this.allocator.allocate(vehicle);
if (!spot) throw new NoAvailableSpotException(`No available spot for vehicle size ${vehicle.getSize()}`);
spot.occupy(vehicle);
return Ticket.create(vehicle, spot, Date.now());
} finally {
this.lock.release();
}
}
async unparkVehicle(ticket: Ticket): Promise<number> {
await this.lock.acquire();
try {
if (ticket.status !== TicketStatus.ACTIVE) throw new InvalidTicketException(`Ticket not active: ${ticket.id}`);
ticket.close(Date.now());
const fee = this.feeStrategy.calculate(ticket);
ticket.spot.vacate();
return fee;
} finally {
this.lock.release();
}
}
}
// ---------- Demo ----------
async function main(): Promise<void> {
const f1Spots: ParkingSpot[] = [
new ParkingSpot("F1-M1", VehicleSize.MOTORCYCLE),
new ParkingSpot("F1-C1", VehicleSize.COMPACT),
];
const floor1 = new Floor(1, f1Spots);
const floors: Floor[] = [floor1];
const allocator: SpotAllocator = new NearestSpotAllocator(floors);
const feeStrategy: FeeStrategy = new HourlyFeeStrategy(10.0);
const lot = new ParkingLot(floors, allocator, feeStrategy);
const car = new Car("KA-01-1234");
const t1 = await lot.parkVehicle(car);
console.log(`Parked car, ticket=${t1.id} spot=${t1.spot.getId()}`);
const bike = new Motorcycle("KA-01-5678");
const t2 = await lot.parkVehicle(bike);
console.log(`Parked bike, ticket=${t2.id} spot=${t2.spot.getId()}`);
try {
const bus = new Bus("KA-01-9999");
await lot.parkVehicle(bus); // no LARGE spot exists -> should fail
} catch (e) {
console.log(`Expected failure parking bus: ${(e as Error).message}`);
}
const fee = await lot.unparkVehicle(t1);
console.log(`Unparked car, fee=${fee}`);
}
main();
export {
VehicleSize, Vehicle, Motorcycle, Car, Bus, ParkingSpot, Floor, Ticket, TicketStatus,
SpotAllocator, FirstAvailableAllocator, NearestSpotAllocator,
FeeStrategy, HourlyFeeStrategy, PeakHourFeeStrategy,
NoAvailableSpotException, InvalidTicketException, ParkingLot,
};