Design a Parking Lot, Low Level Design (LLD) Interview
Object-oriented design of a multi-level parking lot with spot types, ticketing, pluggable pricing, nearest-spot allocation, and payment, built to be safe under concurrent entry and exit.
Asked at: Asked in machine-coding / LLD rounds at Amazon, Uber, Flipkart, Swiggy, PhonePe, Walmart, Atlassian, and most product companies hiring SDE1 to SDE3 in India. It is the most common warm-up LLD problem and a frequent 90-minute machine-coding task.
Why this is asked
It is the cleanest test of core OOD skills: can you take a messy real-world description, carve it into the right classes with single responsibilities, model has-a vs is-a relationships correctly, and pick design patterns for the parts that genuinely vary (pricing, spot allocation, payment). It also exposes whether a candidate thinks about concurrency, because two cars racing for the last spot is the obvious failure mode. There is enough surface to extend (EV charging, reservations, multiple gates) so interviewers can scale difficulty, yet it is small enough to write real working code inside the round.
Requirements
Functional
- A vehicle (car, bike, truck, EV) enters through an entry gate and receives a ticket with entry time and the assigned spot.
- The lot has multiple floors/levels, each with many parking spots of different types: compact, large, handicapped, motorcycle, and EV (with charger).
- On entry the system finds and assigns the nearest available spot that fits the vehicle type, then marks that spot occupied.
- On exit at an exit gate the system computes the fee from parked duration using a configurable pricing policy, takes payment, frees the spot, and closes the ticket.
- Support multiple entry and exit gates operating at the same time.
- Show real-time availability per floor and per spot type (e.g. a display board at the entrance).
- Support different payment methods (cash, card, UPI).
- Reject entry gracefully when the lot, or the relevant spot type, is full.
Constraints & non-functional
- Two vehicles arriving at nearly the same instant must never be assigned the same physical spot (no double-allocation under concurrency).
- A ticket id and a spot must each be in exactly one consistent state at any time; no lost or duplicated tickets.
- Spot-to-vehicle fit rules are fixed: a motorcycle fits a motorcycle/compact/large spot, a car fits compact/large, a truck needs large, an EV needs an EV spot to charge but may park in a normal spot without charging.
- Pricing, spot-selection strategy, and payment method must be swappable without editing the core flow (open for extension, closed for modification).
- Allocation should be O(1) or close to it, not a full scan of every spot on every entry, so the design must keep availability indexed.
- This is in-memory LLD: no database schema, no sharding, no distributed coordination beyond locks; persistence is abstracted behind a repository if asked.
Core classes & entities
ParkingLot
Top-level facade and single source of truth for the whole lot. Owns the floors, the gates, and the active-ticket registry. Coordinates entry and exit but delegates allocation, pricing, and payment to injected strategies. Usually a Singleton because there is one physical lot per process.
attrs: id, name, floors: List<ParkingFloor>, entryGates: List<EntryGate>, exitGates: List<ExitGate>, activeTickets: Map<ticketId, Ticket>, spotAllocationStrategy: SpotAllocationStrategy, pricingStrategy: PricingStrategy
methods: parkVehicle(vehicle, entryGate): Ticket, unparkVehicle(ticketId, exitGate, paymentMethod): Receipt, getAvailability(): Map<VehicleType, int>, isFull(vehicleType): boolean
ParkingFloor
One physical level. Holds all spots on that floor and keeps an index of free spots by type so allocation is O(1) instead of a scan. Reports its own availability to the display board.
attrs: floorNumber, spots: Map<spotId, ParkingSpot>, availableByType: Map<SpotType, Deque<ParkingSpot>>, displayBoard: DisplayBoard
methods: getNearestFreeSpot(vehicleType): Optional<ParkingSpot>, reserveSpot(spot): boolean, releaseSpot(spot): void, getFreeCount(spotType): int
ParkingSpot
A single physical spot. Knows its type, where it is (for distance), whether it is free, and which vehicle occupies it. Enforces the fit rule via canFit(). Guards its own free/occupied transition so it cannot be double-booked.
attrs: id, spotType: SpotType, floorNumber, distanceFromEntry, status: SpotStatus, parkedVehicle: Vehicle, hasCharger: boolean
methods: canFit(vehicle): boolean, assign(vehicle): boolean, vacate(): void, isFree(): boolean
Vehicle
Abstract base for what is being parked. Carries license plate and type. Subclasses (Car, Bike, Truck, ElectricCar) only differ by VehicleType, so this is light inheritance.
attrs: licensePlate, vehicleType: VehicleType, needsCharging: boolean
methods: getType(): VehicleType
Ticket
The contract created at entry and settled at exit. Binds a vehicle to a spot with an entry timestamp, then records exit time and amount once paid. Carries the parking state machine (ACTIVE, PAID, CLOSED).
attrs: id, vehicle, spot, entryGateId, entryTime, exitTime, amount, status: TicketStatus
methods: markPaid(amount, exitTime): void, getDurationMinutes(): long, isActive(): boolean
Gate
Abstract entry/exit point. EntryGate triggers parkVehicle; ExitGate triggers unparkVehicle. Modeling gates explicitly is what makes nearest-spot meaningful and what makes the concurrency story concrete (many gates, one lot).
attrs: id, gateType: GateType, location
methods: process(...)
SpotAllocationStrategy
Pluggable algorithm that picks which free spot to give out: nearest to the entry gate, or first-available, or balanced across floors. Lets the selection policy change without touching ParkingLot.
methods: findSpot(lot, vehicle, entryGate): Optional<ParkingSpot>
PricingStrategy
Pluggable fee calculation. FlatHourly, ProgressiveSlab (first hour cheap then steeper), DayPass, etc. The core exit flow just calls calculatePrice(ticket); the rule lives here.
methods: calculatePrice(ticket): Money
PaymentProcessor
Takes the computed amount through a chosen payment method and returns success/failure. Abstracts cash/card/UPI behind one interface so adding a method does not touch exit logic.
attrs: paymentMethod: PaymentMethod
methods: pay(amount): PaymentResult
DisplayBoard
Read model of availability shown at the entrance. Observes floor/spot changes and reflects free counts per type, so the entrance display stays in sync without the lot pushing to it imperatively.
attrs: floorNumber, freeCounts: Map<SpotType, int>
methods: update(spotType, freeCount): void, render(): String
Relationships
- ParkingLot → composition → ParkingFloor. A lot owns its floors; floors do not exist without the lot.
- ParkingFloor → composition → ParkingSpot. Each floor owns its physical spots; destroying the floor destroys the spots.
- ParkingSpot → association → Vehicle. A spot references the vehicle currently parked on it; the vehicle lives independently of the spot.
- Car → inheritance → Vehicle. Car, Bike, Truck, ElectricCar are concrete subclasses of the abstract Vehicle; they differ only by VehicleType and charging need.
- Ticket → association → Vehicle. A ticket points at the vehicle it was issued for.
- Ticket → association → ParkingSpot. A ticket records which spot was assigned so exit can free exactly that spot.
- ParkingLot → aggregation → Ticket. The lot keeps a registry of active tickets but a ticket is a value-like record with its own lifecycle.
- ParkingLot → association → SpotAllocationStrategy. The lot holds an injected allocation strategy (Strategy pattern); it can be swapped at runtime.
- ParkingLot → association → PricingStrategy. The lot holds an injected pricing strategy; the fee rule is decided here, not hard-coded in exit.
- EntryGate → inheritance → Gate. EntryGate and ExitGate specialize the abstract Gate.
- ParkingLot → aggregation → Gate. The lot has a set of entry and exit gates.
- ParkingFloor → composition → DisplayBoard. Each floor owns a display board that reflects its availability (Observer relationship in behavior).
- PaymentProcessor → association → PaymentMethod. The processor is configured with a concrete payment method strategy (cash/card/UPI).
Design patterns used
Strategy in SpotAllocationStrategy (nearest vs first-fit vs balanced), PricingStrategy (flat hourly vs progressive slab vs day pass), and the payment method.
These three are exactly the parts that change between lots and between interview follow-ups. Encapsulating each as an interface injected into ParkingLot keeps the core entry/exit flow closed for modification but open for extension. Adding a weekend-surge price or a load-balanced allocator means writing one new class, not editing the orchestrator.
Factory Method in VehicleFactory creating Car/Bike/Truck/ElectricCar from input, and a SpotFactory when seeding floors with mixed spot types.
Centralizes the new-object decision so the rest of the code depends on the Vehicle/ParkingSpot abstractions, not concrete classes. The gate just asks the factory for a vehicle by type and stays ignorant of subclasses.
Singleton in ParkingLot itself (one physical lot per running system).
All gates must mutate one shared availability index; multiple instances would let two gates allocate from inconsistent state. A single, thread-safe instance is the natural model. (Stated with the caveat that pure singletons hurt testability, so it is often replaced by a single DI-managed instance.)
Observer in DisplayBoard subscribing to floor/spot availability changes.
The board should not poll. When a spot flips free/occupied it notifies subscribers, so the entrance display and any monitoring stay in sync with one source of truth, decoupling the read model from the write path.
State in Ticket lifecycle (ACTIVE PAID CLOSED) and ParkingSpot status (FREE RESERVED OCCUPIED).
Makes illegal transitions explicit and impossible. You cannot close an unpaid ticket or vacate a free spot, which removes a whole class of bugs that plague boolean-flag implementations.
Facade in ParkingLot.parkVehicle / unparkVehicle as the single entry point.
Clients (gates) call two simple methods; the orchestration of allocation, ticketing, pricing, and payment is hidden behind the facade, keeping callers simple.
Enums
Key API / methods
Ticket parkVehicle(Vehicle vehicle, EntryGate gate)Finds the nearest fitting free spot via the allocation strategy, atomically reserves it, issues an ACTIVE ticket with entry time, registers it, and updates availability. Throws LotFullException if no spot of a fitting type is free.
Receipt unparkVehicle(String ticketId, ExitGate gate, PaymentMethod method)Looks up the active ticket, computes the fee with the pricing strategy from parked duration, charges via the payment processor, then frees the spot and moves the ticket to CLOSED. Throws if ticket is unknown, already closed, or payment fails.
Optional<ParkingSpot> SpotAllocationStrategy.findSpot(ParkingLot lot, Vehicle vehicle, EntryGate gate)Returns the spot to assign, or empty if none fits. The nearest-spot implementation reads each floor's per-type free index and picks the smallest distance, optionally starting from the gate's floor.
Money PricingStrategy.calculatePrice(Ticket ticket)Pure function of the ticket: derives billable units from entry/exit time and spot type and returns the amount. Stateless so it is trivially testable and swappable.
boolean ParkingSpot.assign(Vehicle vehicle)Compare-and-set on spot status from RESERVED/FREE to OCCUPIED; returns false if another thread already took it. This is the concurrency safety point.
Map<VehicleType,Integer> getAvailability()Aggregates free counts across floors per type for the display board and for isFull() checks.
Code skeleton
// ---------- Enums ----------
enum VehicleType { MOTORCYCLE, CAR, TRUCK, ELECTRIC_CAR }
enum SpotType { MOTORCYCLE, COMPACT, LARGE, HANDICAPPED, ELECTRIC }
enum SpotStatus { FREE, RESERVED, OCCUPIED, OUT_OF_SERVICE }
enum TicketStatus { ACTIVE, PAID, CLOSED, LOST }
enum PaymentMethod { CASH, CARD, UPI }
// ---------- Vehicle hierarchy ----------
abstract class Vehicle {
protected final String licensePlate;
protected final VehicleType type;
Vehicle(String plate, VehicleType type) { this.licensePlate = plate; this.type = type; }
VehicleType getType() { return type; }
boolean needsCharging() { return type == VehicleType.ELECTRIC_CAR; }
}
class Car extends Vehicle { Car(String p) { super(p, VehicleType.CAR); } }
class Bike extends Vehicle { Bike(String p) { super(p, VehicleType.MOTORCYCLE); } }
class Truck extends Vehicle{ Truck(String p){ super(p, VehicleType.TRUCK); } }
class ElectricCar extends Vehicle { ElectricCar(String p){ super(p, VehicleType.ELECTRIC_CAR); } }
// ---------- ParkingSpot (concurrency-safe transition) ----------
class ParkingSpot {
final String id; final SpotType spotType; final int floor; final int distanceFromEntry;
final boolean hasCharger;
private volatile SpotStatus status = SpotStatus.FREE;
private Vehicle parkedVehicle;
boolean canFit(Vehicle v) {
switch (v.getType()) {
case MOTORCYCLE: return true; // fits any
case CAR: return spotType == SpotType.COMPACT || spotType == SpotType.LARGE;
case TRUCK: return spotType == SpotType.LARGE;
case ELECTRIC_CAR: return spotType == SpotType.ELECTRIC || spotType == SpotType.COMPACT
|| spotType == SpotType.LARGE;
default: return false;
}
}
// compare-and-set: only the first caller wins the spot
synchronized boolean assign(Vehicle v) {
if (status != SpotStatus.FREE && status != SpotStatus.RESERVED) return false;
this.parkedVehicle = v; this.status = SpotStatus.OCCUPIED; return true;
}
synchronized void vacate() { this.parkedVehicle = null; this.status = SpotStatus.FREE; }
boolean isFree() { return status == SpotStatus.FREE; }
}
// ---------- Strategy interfaces ----------
interface SpotAllocationStrategy {
Optional<ParkingSpot> findSpot(ParkingLot lot, Vehicle vehicle, EntryGate gate);
}
interface PricingStrategy { long calculatePrice(Ticket ticket); }
interface PaymentProcessor { PaymentStatus pay(long amount); }
// Nearest-spot allocation: scan per-floor free index, smallest distance wins
class NearestSpotStrategy implements SpotAllocationStrategy {
public Optional<ParkingSpot> findSpot(ParkingLot lot, Vehicle v, EntryGate gate) {
return lot.getFloors().stream()
.flatMap(f -> f.candidateFreeSpots(v).stream())
.min(Comparator.comparingInt(s -> s.distanceFromEntry));
}
}
// Progressive pricing: first hour flat, then per-hour, rounded up
class ProgressivePricing implements PricingStrategy {
public long calculatePrice(Ticket t) {
long mins = t.getDurationMinutes();
if (mins <= 10) return 0; // grace period
long hours = (long) Math.ceil(mins / 60.0);
long rate = (t.getSpot().spotType == SpotType.LARGE) ? 40 : 20;
return 20 + Math.max(0, hours - 1) * rate; // base + per extra hour
}
}
// ---------- Ticket with state guard ----------
class Ticket {
final String id; final Vehicle vehicle; final ParkingSpot spot; final Instant entryTime;
Instant exitTime; long amount; TicketStatus status = TicketStatus.ACTIVE;
long getDurationMinutes() {
return Duration.between(entryTime, exitTime != null ? exitTime : Instant.now()).toMinutes();
}
void markPaid(long amt, Instant exit) {
if (status != TicketStatus.ACTIVE) throw new IllegalStateException("ticket not active");
this.amount = amt; this.exitTime = exit; this.status = TicketStatus.PAID;
}
}
// ---------- ParkingLot facade (Singleton-ish, thread-safe) ----------
class ParkingLot {
private final List<ParkingFloor> floors;
private final SpotAllocationStrategy allocation;
private final PricingStrategy pricing;
private final Map<String, Ticket> activeTickets = new ConcurrentHashMap<>();
Ticket parkVehicle(Vehicle vehicle, EntryGate gate) {
// retry loop handles the race for the last spot
for (int attempt = 0; attempt < 3; attempt++) {
Optional<ParkingSpot> chosen = allocation.findSpot(this, vehicle, gate);
if (chosen.isEmpty()) throw new LotFullException(vehicle.getType());
ParkingSpot spot = chosen.get();
if (spot.assign(vehicle)) { // atomic win
Ticket t = new Ticket(UUID.randomUUID().toString(), vehicle, spot, Instant.now());
activeTickets.put(t.id, t);
spot.floorRef().notifyAvailabilityChanged(spot.spotType);
return t;
} // else someone beat us; loop and try the next nearest
}
throw new LotFullException(vehicle.getType());
}
Receipt unparkVehicle(String ticketId, ExitGate gate, PaymentProcessor processor) {
Ticket t = activeTickets.get(ticketId);
if (t == null || t.status != TicketStatus.ACTIVE)
throw new InvalidTicketException(ticketId); // idempotent: no double charge
long amount = pricing.calculatePrice(t.withExit(Instant.now()));
if (processor.pay(amount) != PaymentStatus.SUCCESS)
throw new PaymentFailedException(ticketId); // spot stays held until paid
t.markPaid(amount, Instant.now());
t.getSpot().vacate(); // free ONLY after payment
t.getSpot().floorRef().notifyAvailabilityChanged(t.getSpot().spotType);
t.status = TicketStatus.CLOSED;
activeTickets.remove(ticketId);
return new Receipt(ticketId, amount, t.exitTime);
}
}How it works
Start by treating ParkingLot as a facade and a Singleton: there is one physical lot, and every gate must see one shared view of which spots are free. The lot composes floors, each floor composes its spots, and each spot knows its type, distance from the entry, and status. That composition chain is the backbone; nothing about a spot makes sense outside its floor, so it is true ownership, not loose association.
On entry, an EntryGate calls parkVehicle(vehicle, gate). The lot does not itself decide which spot to give out. It delegates to an injected SpotAllocationStrategy. The default NearestSpotStrategy walks each floor's per-type free index, filters by canFit() so a truck never lands in a compact spot, and returns the spot with the smallest distance from that gate. Keeping a free-spot index per type per floor is what makes this close to O(1) instead of scanning thousands of spots on every car. Once a candidate is chosen, the lot calls spot.assign(vehicle), which is a synchronized compare-and-set: it flips FREE/RESERVED to OCCUPIED and returns true only for the first caller. If two cars from two gates picked the same last spot, exactly one wins; the loser's assign returns false and the lot loops to try the next nearest spot. This retry loop is the heart of the concurrency answer and it is what separates a passing design from a hand-wavy one.
When assign succeeds, the lot creates a Ticket in ACTIVE state with the entry timestamp, registers it in a ConcurrentHashMap of active tickets, and tells the floor its availability changed. The floor notifies its DisplayBoard (Observer), so the entrance count updates without polling. The candidate now has a ticket bound to a specific vehicle and a specific spot.
On exit, an ExitGate calls unparkVehicle(ticketId, gate, processor). The lot looks up the active ticket; if it is missing or already CLOSED it throws, which makes a replayed or double exit a no-op rather than a double charge. It stamps the exit time and asks the injected PricingStrategy to compute the fee purely from the ticket's duration and spot type. Crucially, it only frees the spot and closes the ticket after payment returns SUCCESS. If payment fails, the spot stays held and the ticket stays ACTIVE, so nobody drives off on an unpaid, freed spot. After a successful charge, the spot is vacated, the ticket moves ACTIVE to PAID to CLOSED, the registry entry is removed, and the floor again notifies the board. The three swappable pieces (allocation, pricing, payment) are all Strategy interfaces injected into the lot, so a weekend-surge price or a load-balanced allocator is a new class, never an edit to parkVehicle or unparkVehicle. That is the open-closed payoff the interviewer is looking for.
Edge cases & gotchas
- Two cars race for the last compact spot from different gates. Solved by reserving the spot atomically (compare-and-set on SpotStatus or a per-floor lock); the loser retries the allocation strategy for the next spot.
- Lot or a specific spot type is full. Return a clean LotFullException / null Optional and have the gate refuse entry, not a 500 or a half-created ticket.
- Vehicle-spot fit edge: a motorcycle should be allowed into a compact or large spot when its own type is exhausted (fallback ladder), but a truck must never be squeezed into a compact spot. canFit() encodes this asymmetric rule.
- EV with no EV spot free: allow parking in a normal spot but mark charging unavailable, do not block entry just because chargers are taken.
- Lost ticket: vehicle present but ticketId unknown. Move ticket to LOST, charge a flat lost-ticket penalty using vehicle lookup by plate, and still free the spot.
- Free duration / grace period: a car that leaves within, say, 10 minutes pays zero. The pricing strategy must handle a zero or rounded-up first slab correctly (off-by-one on the first hour is the classic bug).
- Clock and rounding: partial hours must round consistently (ceil to the hour) and the same Clock must be injected for entry and exit so tests are deterministic.
- Double exit / replayed exit scan: unparking an already-CLOSED ticket must be idempotent and not double-charge or free a spot now occupied by someone else.
- Spot taken out of service for maintenance: status OUT_OF_SERVICE removes it from the free index so it is never allocated, without deleting it.
- Payment failure mid-exit: do not free the spot or close the ticket until payment SUCCESS, otherwise a non-payer drives off with a freed spot and an unpaid ticket.