Skip to content

36 — LLD: Parking Lot

Frequently Asked at Uber — Reported repeatedly in recent L5A R3 (Coding-Depth) rounds.

Understanding the Problem

The canonical LLD interview — a multi-floor parking lot with different spot types, dynamic pricing, and concurrent entry and exit. The question is beloved because it lets you showcase Strategy, Factory, and lock-free concurrency patterns in a short runway. The trap is building one mega-class that mixes allocation, pricing, and payment.

What is a Parking Lot System?

A lot has multiple floors. Each floor has spots of different types — two-wheeler, four-wheeler, EV with charger. Vehicles enter, get matched to a compatible spot, hold the spot until they leave, then pay based on time and spot type.


Requirements

Clarifying Questions

You: What's the allocation policy — nearest-to-entry, fill-by-floor, random?

Interviewer: Nearest-to-entry. But design for a swappable policy.

Strategy pattern is the natural fit. AllocationStrategy interface with a default NearestEntry implementation.

You: Pricing — flat per-hour, or tiered?

Interviewer: Tiered. First hour at a base rate, then per-15-minute increments. Different rates per spot type.

Another strategy. PricingStrategy.compute(type, duration) -> amount.

You: Can an EV park in a four-wheeler spot if no EV spot is free?

Interviewer: No — EV to EV only. Keeps regular spots free for non-EVs.

Strict matching. No size-up fallback for this version.

You: Lost ticket?

Interviewer: Out of scope.

You: Thread safety?

Interviewer: Mandatory. Concurrent entries and exits from different cars must not interfere.

Final Requirements

  1. entry(Vehicle) -> Ticket — find a compatible spot, mark it occupied, issue a ticket.
  2. exit(ticketId, paymentMethod) -> Receipt — compute fee from duration + spot type, free the spot.
  3. available(SpotType) -> int — live count by type.
  4. Strict vehicle-to-spot matching: 2-wheelers to 2-wheeler spots, 4-wheelers to 4-wheeler spots, EVs to EV spots only.
  5. Thread-safe under concurrent entries and exits.

Out of scope: Lost tickets, reservations, license-plate cameras, payment provider integration details.

Deferred tradeoff: The retry loop in entry handles CAS losses; it can theoretically spin — but with 300+ spots per floor and a bounded retry count, you cap failure modes cleanly.


Core Entities and Relationships

Walking the nouns: Vehicle, Spot, Floor, Lot, Ticket, Receipt, PaymentMethod. Some are pure data (ticket, receipt). Others own behavior: the spot owns its occupancy CAS; the lot owns the orchestration.

EntityResponsibility
VehicleValue: plate, required spot type.
SpotTypeTWO_WHEELER, FOUR_WHEELER, EV.
ParkingSpotid, floor, type, occupant ticket (atomic). Owns its own CAS.
FloorSpots on a floor, grouped by type. Per-type counters.
ParkingLotAll floors; exposes entry/exit/available. Facade.
AllocationStrategyPicks a spot for a vehicle.
PricingStrategyFee calculation given duration + type.
Ticket, ReceiptValue records.
VehicleFactoryBuilds typed vehicles from raw input.
PaymentProcessorCaptures payment (external).

Why does ParkingSpot own the CAS? Because occupancy is a per-spot invariant. A lot-wide lock serializes all entries; a floor-wide lock serializes per floor. Spot-level CAS gives maximum parallelism — two cars parking on the same floor in different spots never contend.

Design is iterative. If EVs later need a separate charger availability state, we will split Spot and Charger — but for now the EV spot is the charger.


Class Design

ParkingSpot

State:

  • id, floor, type — immutable.
  • occupantTicket: AtomicReference<String> — null when free.

Methods:

  • tryOccupy(ticketId) -> boolean — CAS from null to the ticket id.
  • release() — sets occupant back to null.
  • isFree() -> boolean.

AllocationStrategy

Optional<ParkingSpot> allocate(List<Floor> floors, Vehicle v)

Concrete implementation NearestEntryAllocation scans floors in order, then spots within the requested type.

PricingStrategy

BigDecimal compute(SpotType t, Duration parked)

ParkingLot

State:

  • floors: List<Floor>.
  • allocator: AllocationStrategy, pricing: PricingStrategy, paymentProcessor.
  • openTickets: ConcurrentHashMap<String, Ticket>.
  • spotsById: Map<String, ParkingSpot> for O(1) exit lookup.

Methods:

  • entry(Vehicle) -> Ticket.
  • exit(ticketId, paymentMethod) -> Receipt.
  • available(SpotType) -> int.
java
public enum SpotType { TWO_WHEELER, FOUR_WHEELER, EV }

public record Vehicle(String plate, SpotType requiredSpot) {}

public class ParkingSpot {
    final String id;
    final int floor;
    final SpotType type;
    final AtomicReference<String> occupantTicket = new AtomicReference<>();
    public ParkingSpot(String id, int floor, SpotType t) { this.id=id; this.floor=floor; this.type=t; }
    public boolean tryOccupy(String ticketId) {
        return occupantTicket.compareAndSet(null, ticketId);
    }
    public void release() { occupantTicket.set(null); }
    public boolean isFree() { return occupantTicket.get() == null; }
}

public interface AllocationStrategy {
    Optional<ParkingSpot> allocate(List<Floor> floors, Vehicle v);
}

public class NearestEntryAllocation implements AllocationStrategy {
    public Optional<ParkingSpot> allocate(List<Floor> floors, Vehicle v) {
        for (Floor f : floors) {
            for (ParkingSpot s : f.spotsByType(v.requiredSpot())) {
                if (s.isFree()) return Optional.of(s);
            }
        }
        return Optional.empty();
    }
}

public interface PricingStrategy {
    BigDecimal compute(SpotType t, Duration parked);
}

public record Ticket(String id, String plate, String spotId, Instant entry) {}
public record Receipt(String ticketId, BigDecimal amount, Instant exit) {}

public class ParkingLot {
    private final List<Floor> floors;
    private final AllocationStrategy allocator;
    private final PricingStrategy pricing;
    private final Map<String, Ticket> openTickets = new ConcurrentHashMap<>();
    private final Map<String, ParkingSpot> spotsById;

    public Ticket entry(Vehicle v) { /* ... */ }
    public Receipt exit(String ticketId, PaymentMethod pm) { /* ... */ }
}
cpp
enum class SpotType { TWO_WHEELER, FOUR_WHEELER, EV };

struct Vehicle { std::string plate; SpotType required; };

class ParkingSpot {
    std::string id_;
    int floor_;
    SpotType type_;
    std::atomic<bool> occupied{false};
    std::string occupantTicketId;
public:
    ParkingSpot(std::string i, int f, SpotType t) : id_(std::move(i)), floor_(f), type_(t) {}
    bool tryOccupy(const std::string& tid) {
        bool expected = false;
        if (occupied.compare_exchange_strong(expected, true)) {
            occupantTicketId = tid;
            return true;
        }
        return false;
    }
    void release() { occupantTicketId.clear(); occupied.store(false); }
    bool isFree() const { return !occupied.load(); }
    SpotType getType() const { return type_; }
    const std::string& id() const { return id_; }
};
typescript
export enum SpotType { TWO_WHEELER = "TWO", FOUR_WHEELER = "FOUR", EV = "EV" }

export interface Vehicle { plate: string; required: SpotType; }

export class ParkingSpot {
  private occupantTicketId: string | null = null;
  constructor(public id: string, public floor: number, public type: SpotType) {}
  tryOccupy(ticketId: string): boolean {
    if (this.occupantTicketId !== null) return false;
    this.occupantTicketId = ticketId;
    return true;
  }
  release(): void { this.occupantTicketId = null; }
  isFree(): boolean { return this.occupantTicketId === null; }
}

Design principles at play:

  • Strategy: AllocationStrategy and PricingStrategy are open for extension, closed for modification.
  • Factory: VehicleFactory hides construction of typed vehicles behind a single entry point.
  • Single Responsibility: ParkingSpot guards its own occupancy; ParkingLot orchestrates; pricing and allocation live in separate strategies.
  • Lock-free core: CAS on the spot, no lot-wide or floor-wide lock.

Implementation

Core Logic: entry

The subtlety: the allocator returns a candidate spot based on a read. Between read and CAS, another thread can grab it. So wrap in a bounded retry loop.

  1. Call allocator.allocate(floors, vehicle). If empty, throw (lot full).
  2. Attempt spot.tryOccupy(ticketId). On success, record the ticket, return.
  3. On failure (someone else won), retry up to N times.
  4. If all retries fail, throw a contention error.

Core Logic: exit

  1. Remove the ticket from openTickets. If missing, throw.
  2. Look up the spot by id.
  3. Compute duration = now - ticket.entry.
  4. Compute fee via pricing strategy.
  5. Capture payment.
  6. Release the spot.
  7. Return receipt.

From Naive to Great

Bad Solution: Lot-wide lock on entry and exit. Every car serializes through one mutex — awful for a busy lot.

Good Solution: Per-floor lock. Better, but still serializes within a floor. A 10-spot floor bottlenecks at one entry at a time.

Great Solution: CAS per spot. Lock-free. Two cars parking in different spots on the same floor never contend. Counters for available() maintained as AtomicInteger updated on each occupy/release — so the display board does not iterate spots.

Implementations

java
public Ticket entry(Vehicle v) {
    for (int attempt = 0; attempt < 5; attempt++) {
        Optional<ParkingSpot> pick = allocator.allocate(floors, v);
        if (pick.isEmpty()) throw new IllegalStateException("lot full for " + v.requiredSpot());
        ParkingSpot spot = pick.get();
        String ticketId = UUID.randomUUID().toString();
        if (spot.tryOccupy(ticketId)) {
            Ticket t = new Ticket(ticketId, v.plate(), spot.id, Instant.now());
            openTickets.put(ticketId, t);
            return t;
        }
        // someone else took it — refresh allocation
    }
    throw new IllegalStateException("contention: could not allocate after retries");
}

public Receipt exit(String ticketId, PaymentMethod pm) {
    Ticket t = openTickets.remove(ticketId);
    if (t == null) throw new NoSuchElementException("invalid ticket");
    ParkingSpot spot = spotsById.get(t.spotId());
    Duration parked = Duration.between(t.entry(), Instant.now());
    BigDecimal fee = pricing.compute(spot.type, parked);
    paymentProcessor.capture(pm, fee);
    spot.release();
    return new Receipt(ticketId, fee, Instant.now());
}
cpp
std::optional<Ticket> ParkingLot::entry(const Vehicle& v) {
    for (int attempt = 0; attempt < 5; ++attempt) {
        auto pick = allocator->allocate(floors, v);
        if (!pick) return std::nullopt;
        auto spot = *pick;
        std::string tid = newUuid();
        if (spot->tryOccupy(tid)) {
            Ticket t{tid, v.plate, spot->id(), now()};
            openTickets[tid] = t;
            return t;
        }
    }
    return std::nullopt;
}
typescript
entry(v: Vehicle): Ticket {
  for (let attempt = 0; attempt < 5; attempt++) {
    const spot = this.allocator.allocate(this.floors, v);
    if (!spot) throw new Error("lot full");
    const ticketId = crypto.randomUUID();
    if (spot.tryOccupy(ticketId)) {
      const t: Ticket = { id: ticketId, plate: v.plate, spotId: spot.id, entry: Date.now() };
      this.openTickets.set(ticketId, t);
      return t;
    }
  }
  throw new Error("contention");
}

exit(ticketId: string, paymentMethod: string): Receipt {
  const t = this.openTickets.get(ticketId);
  if (!t) throw new Error("invalid ticket");
  this.openTickets.delete(ticketId);
  const spot = this.spotsById.get(t.spotId)!;
  const parkedMs = Date.now() - t.entry;
  const fee = this.pricing.compute(spot.type, parkedMs);
  // payment capture (external)
  spot.release();
  return { ticketId, amount: fee, exit: Date.now() };
}

Thread Safety

The entire concurrency story is one line: AtomicReference.compareAndSet(null, ticketId). Everything else follows from that.

  • No lot-wide lock. No floor-wide lock. Cars on different spots parallelize fully.
  • openTickets is a ConcurrentHashMap.
  • Counts per type per floor are AtomicInteger updated on occupy/release. Avoid iterating spots to count — it races with mutations and is O(N).

Locking a Floor as a whole would be a bottleneck — resist the temptation.

Verification

Two cars enter a lot with 1 four-wheeler spot. One exits at t=1:30.

TimeOpResult
0C1 entryallocate → S1; tryOccupy → true; ticket T1 issued
0C2 entryallocate → S1; tryOccupy → false (C1 won); retry → allocate empty → throw full
1:30C1 exit(T1)fee = pricing.compute(FOUR, 1:30); spot freed
1:31C2 entry retryallocate → S1; tryOccupy → true; ticket T2

Trace for the race at t=0:

  • Both threads see S1 as free in the allocator read.
  • Thread C1 wins the CAS: occupantTicket: null → T1. Returns true.
  • Thread C2's CAS sees T1 (not null) and fails.
  • C2 retries: allocator finds no free spot. Throws.

Complete Code Implementation

java
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;

public enum SpotType { TWO_WHEELER, FOUR_WHEELER, EV }
public record Vehicle(String plate, SpotType requiredSpot) {}
public record Ticket(String id, String plate, String spotId, Instant entry) {}
public record Receipt(String ticketId, BigDecimal amount, Instant exit) {}

public class ParkingSpot {
    public final String id;
    public final int floor;
    public final SpotType type;
    private final AtomicReference<String> occupantTicket = new AtomicReference<>();
    public ParkingSpot(String id, int floor, SpotType t) { this.id=id; this.floor=floor; this.type=t; }
    public boolean tryOccupy(String ticketId) { return occupantTicket.compareAndSet(null, ticketId); }
    public void release() { occupantTicket.set(null); }
    public boolean isFree() { return occupantTicket.get() == null; }
}

public class Floor {
    public final int number;
    private final Map<SpotType, List<ParkingSpot>> spotsByType = new EnumMap<>(SpotType.class);
    private final Map<SpotType, AtomicInteger> free = new EnumMap<>(SpotType.class);
    public Floor(int n, List<ParkingSpot> spots) {
        this.number = n;
        for (SpotType t : SpotType.values()) {
            spotsByType.put(t, new ArrayList<>());
            free.put(t, new AtomicInteger(0));
        }
        for (ParkingSpot s : spots) {
            spotsByType.get(s.type).add(s);
            free.get(s.type).incrementAndGet();
        }
    }
    public List<ParkingSpot> spotsByType(SpotType t) { return spotsByType.get(t); }
    public int available(SpotType t) { return free.get(t).get(); }
    public void noteOccupied(SpotType t) { free.get(t).decrementAndGet(); }
    public void noteReleased(SpotType t) { free.get(t).incrementAndGet(); }
}

public interface AllocationStrategy {
    Optional<ParkingSpot> allocate(List<Floor> floors, Vehicle v);
}

public class NearestEntryAllocation implements AllocationStrategy {
    public Optional<ParkingSpot> allocate(List<Floor> floors, Vehicle v) {
        for (Floor f : floors)
            for (ParkingSpot s : f.spotsByType(v.requiredSpot()))
                if (s.isFree()) return Optional.of(s);
        return Optional.empty();
    }
}

public interface PricingStrategy {
    BigDecimal compute(SpotType t, Duration parked);
}

public class TieredPricing implements PricingStrategy {
    private final Map<SpotType, BigDecimal> firstHour = Map.of(
        SpotType.TWO_WHEELER, new BigDecimal("1.00"),
        SpotType.FOUR_WHEELER, new BigDecimal("2.00"),
        SpotType.EV, new BigDecimal("3.00"));
    private final Map<SpotType, BigDecimal> per15 = Map.of(
        SpotType.TWO_WHEELER, new BigDecimal("0.25"),
        SpotType.FOUR_WHEELER, new BigDecimal("0.50"),
        SpotType.EV, new BigDecimal("0.75"));

    public BigDecimal compute(SpotType t, Duration parked) {
        if (parked.toMinutes() <= 60) return firstHour.get(t);
        long extraQuarter = (parked.toMinutes() - 60 + 14) / 15;
        return firstHour.get(t).add(per15.get(t).multiply(BigDecimal.valueOf(extraQuarter)))
                .setScale(2, RoundingMode.HALF_UP);
    }
}

public class ParkingLot {
    private final List<Floor> floors;
    private final AllocationStrategy allocator;
    private final PricingStrategy pricing;
    private final Map<String, Ticket> openTickets = new ConcurrentHashMap<>();
    private final Map<String, ParkingSpot> spotsById;

    public ParkingLot(List<Floor> floors, AllocationStrategy a, PricingStrategy p) {
        this.floors = floors; this.allocator = a; this.pricing = p;
        Map<String, ParkingSpot> idx = new HashMap<>();
        for (Floor f : floors) for (SpotType t : SpotType.values())
            for (ParkingSpot s : f.spotsByType(t)) idx.put(s.id, s);
        this.spotsById = idx;
    }

    public Ticket entry(Vehicle v) {
        for (int attempt = 0; attempt < 5; attempt++) {
            Optional<ParkingSpot> pick = allocator.allocate(floors, v);
            if (pick.isEmpty()) throw new IllegalStateException("lot full");
            ParkingSpot spot = pick.get();
            String tid = UUID.randomUUID().toString();
            if (spot.tryOccupy(tid)) {
                floors.get(spot.floor).noteOccupied(spot.type);
                Ticket t = new Ticket(tid, v.plate(), spot.id, Instant.now());
                openTickets.put(tid, t);
                return t;
            }
        }
        throw new IllegalStateException("contention");
    }

    public Receipt exit(String ticketId) {
        Ticket t = openTickets.remove(ticketId);
        if (t == null) throw new NoSuchElementException("invalid ticket");
        ParkingSpot spot = spotsById.get(t.spotId());
        Duration parked = Duration.between(t.entry(), Instant.now());
        BigDecimal fee = pricing.compute(spot.type, parked);
        spot.release();
        floors.get(spot.floor).noteReleased(spot.type);
        return new Receipt(ticketId, fee, Instant.now());
    }

    public int available(SpotType t) {
        int total = 0;
        for (Floor f : floors) total += f.available(t);
        return total;
    }
}
cpp
#include <atomic>
#include <chrono>
#include <memory>
#include <optional>
#include <string>
#include <unordered_map>
#include <vector>

enum class SpotType { TWO_WHEELER, FOUR_WHEELER, EV };

class ParkingSpot {
    std::string id_;
    int floor_;
    SpotType type_;
    std::atomic<bool> occupied{false};
    std::string occupantTicketId;
public:
    ParkingSpot(std::string i, int f, SpotType t) : id_(std::move(i)), floor_(f), type_(t) {}
    bool tryOccupy(const std::string& tid) {
        bool expected = false;
        if (occupied.compare_exchange_strong(expected, true)) { occupantTicketId = tid; return true; }
        return false;
    }
    void release() { occupantTicketId.clear(); occupied.store(false); }
    bool isFree() const { return !occupied.load(); }
    SpotType type() const { return type_; }
    int floor() const { return floor_; }
    const std::string& id() const { return id_; }
};

// Floors, strategies, and ParkingLot follow the Java skeleton.
typescript
export class ParkingSpot {
  private occupantTicketId: string | null = null;
  constructor(public id: string, public floor: number, public type: SpotType) {}
  tryOccupy(tid: string): boolean {
    if (this.occupantTicketId !== null) return false;
    this.occupantTicketId = tid;
    return true;
  }
  release(): void { this.occupantTicketId = null; }
  isFree(): boolean { return this.occupantTicketId === null; }
}

export class Floor {
  private spotsByType = new Map<SpotType, ParkingSpot[]>();
  private free = new Map<SpotType, number>();
  constructor(public number: number, spots: ParkingSpot[]) {
    for (const t of Object.values(SpotType)) {
      this.spotsByType.set(t as SpotType, []);
      this.free.set(t as SpotType, 0);
    }
    for (const s of spots) {
      this.spotsByType.get(s.type)!.push(s);
      this.free.set(s.type, (this.free.get(s.type) ?? 0) + 1);
    }
  }
  spots(t: SpotType): ParkingSpot[] { return this.spotsByType.get(t) ?? []; }
  available(t: SpotType): number { return this.free.get(t) ?? 0; }
  noteOccupied(t: SpotType): void { this.free.set(t, (this.free.get(t) ?? 0) - 1); }
  noteReleased(t: SpotType): void { this.free.set(t, (this.free.get(t) ?? 0) + 1); }
}

export interface AllocationStrategy {
  allocate(floors: Floor[], v: Vehicle): ParkingSpot | null;
}

export const nearestEntryAllocation: AllocationStrategy = {
  allocate(floors, v) {
    for (const f of floors)
      for (const s of f.spots(v.required))
        if (s.isFree()) return s;
    return null;
  }
};

export interface PricingStrategy {
  compute(t: SpotType, parkedMs: number): number;
}

export class ParkingLot {
  private openTickets = new Map<string, { id: string; plate: string; spotId: string; entry: number; }>();
  private spotsById = new Map<string, ParkingSpot>();

  constructor(private floors: Floor[], private allocator: AllocationStrategy, private pricing: PricingStrategy) {
    for (const f of floors) for (const t of Object.values(SpotType))
      for (const s of f.spots(t as SpotType)) this.spotsById.set(s.id, s);
  }

  entry(v: Vehicle): { id: string; plate: string; spotId: string; entry: number; } {
    for (let attempt = 0; attempt < 5; attempt++) {
      const s = this.allocator.allocate(this.floors, v);
      if (!s) throw new Error("lot full");
      const tid = crypto.randomUUID();
      if (s.tryOccupy(tid)) {
        this.floors[s.floor].noteOccupied(s.type);
        const t = { id: tid, plate: v.plate, spotId: s.id, entry: Date.now() };
        this.openTickets.set(tid, t);
        return t;
      }
    }
    throw new Error("contention");
  }

  exit(tid: string): { ticketId: string; amount: number; exit: number; } {
    const t = this.openTickets.get(tid);
    if (!t) throw new Error("invalid ticket");
    this.openTickets.delete(tid);
    const s = this.spotsById.get(t.spotId)!;
    const fee = this.pricing.compute(s.type, Date.now() - t.entry);
    s.release();
    this.floors[s.floor].noteReleased(s.type);
    return { ticketId: tid, amount: fee, exit: Date.now() };
  }

  available(t: SpotType): number {
    return this.floors.reduce((sum, f) => sum + f.available(t), 0);
  }
}

Extensibility

1. "Multiple entry/exit gates with different proximity?"

Pass an entryId into allocate. Each entry owns an ordered list of spots by walking distance. The allocator picks from the closest list first.

java
public interface AllocationStrategy {
    Optional<ParkingSpot> allocate(List<Floor> floors, Vehicle v, String entryId);
}

2. "Reserved spots for specific license plates?"

Add reservedFor: String | null to ParkingSpot. tryOccupy checks if reservedFor is non-null and matches the vehicle's plate. Otherwise fail.

Tradeoff: Reserved spots stay empty until their owner arrives. You may want a grace period after which reservation expires.

3. "Loyalty discount on pricing?"

Decorate the pricing strategy: new LoyaltyDiscountPricing(basePricing, userTier). The decorator applies a percentage off the computed amount.

java
public class LoyaltyDiscountPricing implements PricingStrategy {
    private final PricingStrategy base;
    private final BigDecimal discount; // e.g. 0.10 for 10%
    public BigDecimal compute(SpotType t, Duration d) {
        return base.compute(t, d).multiply(BigDecimal.ONE.subtract(discount));
    }
}

4. "EV charger availability separate from spot occupancy?"

Split Spot and Charger. The EV spot has a reference to a Charger with its own state machine. A car can be parked (occupying the spot) without actively charging.


What is Expected at Each Level

LevelExpectations
Junior (L4)One mega-class ParkingLot doing allocation, pricing, and payment. Single global lock.
Mid (L5)Strategy + Factory, per-floor or per-spot lock. Identifies the concurrency hazard during allocation.
Senior (L5A)CAS on spot, no lot-wide lock, bounded retry on contention. Separate AllocationStrategy and PricingStrategy. Maintained per-type counters for available. Clear extension story.
Staff (L6)Multi-site system, reservation service, dynamic pricing based on demand, license-plate recognition integration, fault tolerance when spot sensors go offline, SLOs for entry/exit latency.

Frontend interview preparation reference.