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.