1. Parking Lot Intermediate → Advanced+Concurrency

A. Realistic Interview Prompt

"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

D. Requirements

Functional

  1. Support multiple vehicle types (motorcycle, car, bus/truck at minimum)
  2. Support multiple spot types/sizes with a compatibility rule between vehicle and spot
  3. Assign an available compatible spot on entry; reject entry if none available
  4. Issue a ticket at entry (unique ID, entry time, assigned spot)
  5. On exit, validate the ticket, compute a fee based on duration, free the spot
  6. Support multiple floors
  7. Track real-time availability, ideally per spot type

Non-Functional

E. Clarifying Questions

#QuestionWhy It MattersDesign Decision Affected
1What vehicle types must be supported, and can new ones be added later?Determines a hardcoded enum vs. an extensible size/category modelVehicle hierarchy, matching logic
2Is matching exact type→type, or by a general "size" concept?Size-based matching scales to new vehicle types with zero new classesVehicleSize enum vs. type-to-type map
3Can 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
4Single-floor or multi-floor from day one?Avoids either premature complexity or a later redesignWhether a Floor layer exists
5Flat hourly, tiered, or size-dependent fee?Determines whether pricing is one formula or needs an abstractionFeeStrategy vs. inline calc
6Can pricing change over time (peak hours, promos)?If yes, pricing must be pluggable, not hardcodedStrategy pattern justification
7Are there multiple entrances/exits operating concurrently?The single biggest driver of concurrency designLocking granularity, atomicity of allocation
8Can a spot be reserved in advance?Adds a reservation lifecycle distinct from walk-in occupancyExtra state on ParkingSpot
9What happens when the lot is full?Simple rejection vs. queue/waitlistError-handling contract on parkVehicle
10Is payment processing in scope, or just fee computation?Avoids building an unrequested payment gateway integrationWhether PaymentProcessor exists
11Must state persist across restarts?Affects ID durability and crash recovery of ticket statePersistence section relevance
12Do 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
13Do lost/duplicate tickets need handling?Affects whether Ticket needs a status/state machineTicket lifecycle design

F. Assumptions

G. Actors

ActorResponsibility
Driver / CustomerInitiates entry (brings a vehicle) and exit (presents ticket)
Lot Operator / AdminConfigures floors/spots — administrative, outside the park/unpark hot path
SystemPerforms allocation, ticketing, fee calculation autonomously — no human decision in the core flow

H. Core Domain Entities

ClassResponsibilityKey AttributesKey MethodsDoes NOT Handle
Vehicle (abstract)Represents a vehicle and its size classlicensePlate, sizegetSize()Spot assignment, pricing
Motorcycle/Car/BusConcrete vehicle — fixes size——Any behavior beyond identity
ParkingSpotRepresents one physical spaceid, size, isOccupiedcanFit(), occupy(), vacate()Knowing about floors, pricing, tickets
FloorGroups spots, tracks per-floor availabilityfloorNumber, spots-by-sizefindAvailableSpot(size)Global allocation policy, pricing
SpotAllocator (interface)Decides which spot to assign across floors—allocate(Vehicle)Occupancy bookkeeping itself
TicketRepresents one parking sessionid, vehicle, spot, entry/exit time, statusclose(exitTime)Fee computation logic
FeeStrategy (interface)Computes fee for a completed session—calculate(Ticket)Payment collection
ParkingLot (facade)Public entry pointfloorsparkVehicle(), 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

J. Class Diagram

ParkingLot

+parkVehicle(Vehicle)

+unparkVehicle(Ticket)

Floor

+findAvailableSpot(VehicleSize)

ParkingSpot

+canFit(Vehicle)

+occupy()

+vacate()

«interface»

SpotAllocator

+allocate(Vehicle)

«interface»

FeeStrategy

+calculate(Ticket)

«abstract»

Vehicle

+getSize()

Ticket

id

entryTime

exitTime

status

NearestSpotAllocator

FirstAvailableAllocator

HourlyFeeStrategy

PeakHourFeeStrategy

Motorcycle

Car

Bus

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

K. Responsibility Mapping

ClassResponsibilityDoes NOT Handle
ParkingLotCoordinates entry/exit, owns floorsPricing math, allocation algorithm, occupancy bit-flipping
FloorFinds available spot on its levelCross-floor policy, pricing
ParkingSpotTracks its own occupancyDeciding who gets assigned to it
SpotAllocatorDecides which spot across all floorsMutating spot state directly (calls spot's own methods)
FeeStrategyComputes a fee for a sessionCollecting/processing payment
TicketRecords one session's factsAny computation — it's a data holder

L. SOLID Analysis

PrincipleHow It's AppliedNaive Violation Example
SRPParkingLot only coordinates; pricing and allocation live in their own classesOne ParkingLot class with calculateFee(), findSpot(), processPayment(), printTicket() all inline
OCPNew vehicle type = new Vehicle subclass; new pricing = new FeeStrategy; zero edits to existing classesif (vehicle.type == "TRUCK") fee = ... else if (...) chains that grow with every new type
LSPAny Vehicle subtype can be passed to parkVehicle() without special-casingA subclass that throws "unsupported" for some method the base type promises
ISPSpotAllocator and FeeStrategy are separate, single-method interfaces — no client is forced to implement methods it doesn't needOne fat ParkingPolicy interface with allocation + pricing + notification methods
DIPParkingLot depends on the SpotAllocator/FeeStrategy abstractions, injected at construction — not concrete implementationsParkingLot directly instantiates new NearestSpotAllocator() internally, making it untestable/unswappable

M. Design Patterns

PatternProblem It SolvesWhere It AppearsAlternativeWhen NOT to Use
StrategyAllocation and pricing logic vary independently and change over timeSpotAllocator, FeeStrategyIf pricing truly never changes: inline a single formula methodIf there is exactly one, permanent algorithm — don't add an interface "just in case"
FacadeCallers shouldn't need to know about Floor/Allocator/FeeStrategy wiringParkingLot.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 / LOSTTicket.statusPlain 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 creationTicketFactory (optional)Plain constructor + UUID callDon'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?

BehaviorCurrent DesignAnticipated ChangeProtecting Abstraction
Spot allocation policyFirst-available within sizeNearest-to-entrance, load-balanced across floorsSpotAllocator interface
PricingFlat hourly by sizePeak hours, subscriptions, weekday/weekend ratesFeeStrategy interface
Vehicle typesMotorcycle/Car/BusEV, oversized, handicapped-onlyVehicle subclassing + size enum
Payment methodNot in scopeCash, card, mobile walletFuture PaymentStrategy interface (not built prematurely)
NotificationNot in scopeSMS/app alert when lot is nearly fullFuture Observer/event bus (not built prematurely)

O. Design Evolution

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.

P. API / Interface Design

class ParkingLot {
  Ticket parkVehicle(Vehicle vehicle);          // throws NoAvailableSpotException
  Fee    unparkVehicle(Ticket ticket);          // throws InvalidTicketException
}

interface SpotAllocator {
  ParkingSpot allocate(VehicleSize size);       // returns null / throws if none available
}

interface FeeStrategy {
  Money calculate(Ticket closedTicket);
}

class ParkingSpot {
  boolean canFit(VehicleSize size);
  void    occupy(Vehicle vehicle);              // throws IllegalStateException if already occupied
  void    vacate();
}

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

R. End-to-End Use Case

Driver
  │  drives up, presents vehicle
  ▼
ParkingLot.parkVehicle(vehicle)
  │
  ▼
SpotAllocator.allocate(vehicle.size)
  │  iterates Floors → Floor.findAvailableSpot(size)
  ▼
ParkingSpot.occupy(vehicle)          // spot now OCCUPIED
  │
  ▼
Ticket.create(vehicle, spot, now)    // returned to driver
  ...time passes...
Driver presents Ticket at exit
  │
  ▼
ParkingLot.unparkVehicle(ticket)
  │
  ▼
FeeStrategy.calculate(ticket)  →  Fee
  │
  ▼
ParkingSpot.vacate()                  // spot now FREE

S. Data Structures

StructureUsed ForWhyAlternative & Trade-off
Map<VehicleSize, List<ParkingSpot>>Available-spot lookup per floorO(1) access to the right size bucketSingle flat list + linear scan — simpler but O(n) per allocation
Map<TicketId, Ticket>Ticket validation on exitO(1) lookup by ticket IDDB-backed lookup if persistence is required (see X)
Min-heap per entrance (only if "nearest spot" is required)Nearest-available-spot allocationO(log n) pop of the closest free spotPlain list + scan — O(n), fine at small scale, avoid the heap unless nearest-spot was explicitly asked for

T. Complexity Analysis

OperationTimeSpace
parkVehicle (first-available)O(1) amortized with size-bucketed mapO(1) extra per ticket
parkVehicle (nearest-spot via heap)O(log n)O(n) for the heap
unparkVehicleO(1)O(1)
Overall spot storage—O(total spots)

U. Testing Strategy

V. Interviewer Follow-Ups

Follow-UpTestingDesign ChangeClasses ChangedClasses UnchangedPattern/Trade-off
"Now support EV charging spots."OCP under a new requirementNew spot behavior (charge while parked)New EVSpot subclass (only one whose behavior actually differs)ParkingLot, SpotAllocatorSubclass only where behavior differs — not every spot type needs one
"Now support reservations."State modelingSpot gains a RESERVED state distinct from OCCUPIED; reservation has an expiryParkingSpot, new Reservation classFeeStrategyCareful 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 conditionsAllocation must be atomic (lock-per-floor or CAS on spot state)ParkingSpot.occupy(), SpotAllocatorExternal API signatureOptimistic (CAS) vs. pessimistic (lock) — see W
"Pricing changes daily based on demand."Recognizing Strategy was the right callNew FeeStrategy implementation, no other changeNew strategy class onlyParkingLot, TicketValidates OCP investment made in V4
"Support millions of parking lots across a city (not just one)."LLD → HLD boundary awarenessThis crosses into HLD: sharding by lot ID, a database instead of in-memory mapsNone in-process — this is a service/infra changeAll core classes — the domain model doesn't change, only where it runsRecognize the boundary rather than trying to "distributed-system-ify" the classes themselves
"The payment provider can fail mid-transaction."Failure handling / idempotencyunparkVehicle must not vacate the spot until payment is confirmed, and must be retry-safeParkingLot.unparkVehicleParkingSpotIdempotency 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 sizecanFit() needs an eligibility check beyond size (e.g. a permit flag)ParkingSpot.canFit(), VehicleSpotAllocator logic itselfDon'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 caseReject if an ACTIVE ticket already exists for that plateParkingLot.parkVehicle—Simple guard, not a new class
"Support monthly subscribers with pre-assigned spots."Spot ownership modelA spot can be reserved-for-owner outside the general poolParkingSpot gains an optional owner reference; allocator excludes owned spots from general poolFeeStrategyCareful 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 awarenessInject a Clock/time source instead of calling now() directlyTicket, FeeStrategy—Dependency injection over static time calls
"What if two floors both report an available compact spot at the same nanosecond?"Cross-floor raceLocking 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.

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

LayerWhat Belongs Here
LLD (this document)Object relationships, responsibilities, APIs, in-process state, patterns for one lot
HLDA ParkingService behind a load balancer, a database per lot or shared with lot_id partitioning, a cache for "current availability" reads
Distributed SystemsOnly 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

AA. Common Bad Designs

AB. Design Trade-Offs

DecisionOption AOption BTrade-off
Vehicle-spot matchingExact type-to-type mapSize-based hierarchyType map is simpler to explain but doesn't generalize; size-based scales to new types for free but requires agreeing on an ordering
Lock granularityOne lock per lotOne lock/atomic per spotCoarse lock = simple, more contention; fine-grained = more parallel, more complex correctness reasoning
Allocation policyFirst-availableNearest-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 poolMap<Size, List<Spot>>Flat List<Spot> + linear scanMap is faster at scale; flat list is simpler to reason about for a small lot and easier to unit test

AC. Interviewer Evaluation

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.
Interviewer: Pricing rules change frequently — think peak hours, promotions.
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.

AE. Online Solution Comparison

SourceApproachStrengthWeaknessWorth Learning
Hello InterviewType-based matching, grouped spot collections, deliberately minimal patternsResists over-patterning; clean baselineConcurrency for multi-entrance is only lightly touchedIts discipline about not adding patterns without justification
Grokking OOD (tssovi, GitHub)Hardware-oriented (panels, display boards, attendants), tiered pricing, Singleton + StateThorough use-case coverage; good State-pattern example for ticket statusSingleton on ParkingLot is a questionable default; more classes than most interviews needThe ticket state-machine idea (ACTIVE/PAID/LOST)
ByteByteGoSize-based matching via VehicleSize, Facade + Strategy chain for pricing, explicit OCP discussionCleanest separation of concerns of the three; explicit extensibility reasoningChained fare strategies add complexity most interviews won't needSize-based matching and the Facade framing — both adopted in this document

Additional cross-referenced sources (search-level only, not deep-fetched): GeeksforGeeks, System Design Handbook.

AF. Interview Cheat Sheet

ProblemParking Lot
DifficultyIntermediate → Advanced+Concurrency
Core requirementsMulti-vehicle, multi-spot, multi-floor; ticket on entry; fee on exit
Key clarificationsSize-based or type-based matching? Pricing variability? Multi-entrance concurrency? Reservations in scope?
Core entitiesVehicle, ParkingSpot, Floor, Ticket, SpotAllocator, FeeStrategy, ParkingLot (facade)
Key relationshipsComposition: Lot→Floor→Spot. Dependency: Lot→Allocator/FeeStrategy (interfaces)
Key patternsStrategy (allocation, pricing), Facade (ParkingLot)
Most important decisionExtracting allocation and pricing behind interfaces before being asked — anticipating change
Likely-changing behaviorPricing rules, allocation policy, vehicle types
Concurrency concernAtomic check-and-occupy on spot allocation under concurrent entries
Top follow-upsEV spots, reservations, concurrent entrances, payment failure, city-scale (HLD boundary)
Common mistakesGod-class ParkingLot; Singleton by default; type-string if/else chains
What NOT to over-engineerPayment gateway, hardware modeling, reservations — unless explicitly asked

AG. Full Implementation

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);
    }
}