Hirestack
Hirestack
Chapter 5

Problem 1: Design a Parking Lot

📖 7 min read · 14 sections · May 2026
Asked at: Amazon Microsoft Atlassian

The setup

"Design the software for a parking lot. The parking lot has multiple floors, multiple spot types (motorcycle, compact, large, EV-charging), and processes ticketed entry/exit with payment. Support multiple payment methods."

This is the canonical LLD interview problem. It tests fundamentals: identifying entities, defining relationships, choosing the right abstractions, and applying the Strategy/Factory patterns. Almost every senior LLD interview at Indian tech companies uses some version of this.

Clarifying questions

QuestionStory answer
How many floors? Spots per floor?10 floors, ~200 spots per floor.
Vehicle types?Motorcycle, Compact (car), Large (SUV/truck), EV-charging spot.
How is pricing calculated?Per hour, varies by vehicle type. Different rate for EV (parking + charging cost).
Payment methods?Cash, Credit Card, UPI. Must be extensible (add new methods later).
What happens if lot is full?Reject entry with a "lot full" message. Optional: maintain a wait queue.
Concurrent access?Yes — many cars may arrive simultaneously. Must handle correctly.
Real-time spot availability dashboard?Out of scope; mention as an extension.

Functional and non-functional requirements

Functional:

Non-functional:

Core entities

EntityResponsibility
ParkingLotAggregates floors; entry point for park/exit operations
ParkingFloorHolds spots for one floor; tracks availability per spot type
ParkingSpotRepresents a single spot; tracks its vehicle (if any), type, location
VehicleRepresents the entering vehicle; has type, license plate
TicketIssued at entry; contains spot info, entry time, vehicle ref
FeeCalculatorComputes fee for a ticket (encapsulates pricing logic)
PaymentMethodInterface; CreditCardPayment, UpiPayment, CashPayment implement it
PaymentProcessorTakes a PaymentMethod and amount; handles processing
SpotAllocatorStrategy for finding the right spot for a vehicle

Class hierarchy

classDiagram
    direction LR
    class ParkingLot {
      +parkVehicle()
      +exitVehicle()
    }
    class Floor {
      +findSpot()
    }
    class Spot {
      +assign()
      +release()
    }
    class Vehicle
    class Car
    class Truck
    class Motorcycle
    class Ticket
    class FeeStrategy
    class PaymentMethod
    ParkingLot *-- Floor
    Floor *-- Spot
    Spot -- Vehicle
    Car ..|> Vehicle
    Truck ..|> Vehicle
    Motorcycle ..|> Vehicle
    ParkingLot -- Ticket
    ParkingLot -- FeeStrategy
    ParkingLot -- PaymentMethod
// Vehicle hierarchy
abstract class Vehicle {
  String licensePlate;
  VehicleType getType();  // MOTORCYCLE | COMPACT | LARGE | EV
}
class Motorcycle extends Vehicle { ... }
class Car extends Vehicle { ... }
class Truck extends Vehicle { ... }
class ElectricCar extends Vehicle { ... }

// Spot hierarchy
abstract class ParkingSpot {
  int spotId;
  SpotType type;
  Vehicle currentVehicle;  // null if free
  boolean canFit(Vehicle v);
  void park(Vehicle v);
  void release();
}
class MotorcycleSpot extends ParkingSpot { ... }
class CompactSpot extends ParkingSpot { ... }
class LargeSpot extends ParkingSpot { ... }
class EVChargingSpot extends ParkingSpot { ... }

Design Decision 1: Spot allocation strategy

How do we pick which spot to give an arriving vehicle? Multiple sub-decisions:

  1. A motorcycle can fit in a motorcycle spot, a compact spot, or a large spot. Prefer smallest fitting spot — leave larger spots for vehicles that need them.
  2. An electric car: prefer EV spot (has charger); fall back to compact/large if EV spots are full.
  3. Truck: must use Large spot.

This is a strategy. Use the Strategy pattern:

interface SpotAllocator {
  ParkingSpot findSpot(Vehicle vehicle, ParkingLot lot);
}

class SmallestFitFirstAllocator implements SpotAllocator {
  public ParkingSpot findSpot(Vehicle v, ParkingLot lot) {
    // Try spot types in order: matching size, then larger
    List preferredTypes = getPreferredTypes(v.getType());
    for (SpotType type : preferredTypes) {
      ParkingSpot spot = lot.findFreeSpot(type);
      if (spot != null) return spot;
    }
    return null;
  }
  
  private List getPreferredTypes(VehicleType v) {
    return switch (v) {
      case MOTORCYCLE -> List.of(MOTORCYCLE_SPOT, COMPACT_SPOT, LARGE_SPOT);
      case COMPACT    -> List.of(COMPACT_SPOT, LARGE_SPOT);
      case LARGE      -> List.of(LARGE_SPOT);
      case EV         -> List.of(EV_SPOT, COMPACT_SPOT, LARGE_SPOT);
    };
  }
}

Why Strategy: future requirements might want different allocation policies — closest spot, nearest exit, premium spots — and we can swap by configuration.

Design Decision 2: Payment methods

Multiple payment methods, each with different processing logic. Strategy pattern again — payment methods are interchangeable strategies.

interface PaymentMethod {
  PaymentResult pay(BigDecimal amount);
}

class CreditCardPayment implements PaymentMethod {
  String cardNumber, cvv, expiry;
  public PaymentResult pay(BigDecimal amount) {
    // Call card processor; return result
  }
}

class UpiPayment implements PaymentMethod {
  String upiId;
  public PaymentResult pay(BigDecimal amount) {
    // Initiate UPI transaction
  }
}

class CashPayment implements PaymentMethod {
  public PaymentResult pay(BigDecimal amount) {
    // Mark as cash received
  }
}

The PaymentProcessor accepts any PaymentMethod:

class PaymentProcessor {
  public PaymentResult process(PaymentMethod method, BigDecimal amount) {
    PaymentResult result = method.pay(amount);
    // log; emit event; etc.
    return result;
  }
}

Adding a new method (BitcoinPayment, ApplePay) is a new class implementing the interface — no modification to existing code. This is Open-Closed in action.

Design Decision 3: Fee calculation

Fees depend on vehicle type and duration. The simple approach is enough for v1:

class FeeCalculator {
  private static final Map hourlyRates = Map.of(
    MOTORCYCLE, new BigDecimal("10"),
    COMPACT,    new BigDecimal("30"),
    LARGE,      new BigDecimal("50"),
    EV,         new BigDecimal("40")  // parking only; charging billed separately
  );
  
  public BigDecimal calculate(Ticket ticket, Instant exitTime) {
    Duration duration = Duration.between(ticket.entryTime, exitTime);
    long hours = (long) Math.ceil(duration.toMinutes() / 60.0);  // round up
    BigDecimal rate = hourlyRates.get(ticket.vehicleType);
    return rate.multiply(BigDecimal.valueOf(hours));
  }
}

For more complex pricing (time-of-day pricing, weekly subscription, VIP rates), introduce a PricingStrategy interface. Don't over-engineer for v1.

Design Decision 4: Concurrency

Multiple vehicles can arrive simultaneously. Two cars trying to take the last compact spot must result in exactly one parking, the other rejected.

class ParkingFloor {
  private final Map> freeSpots;
  private final Lock lock = new ReentrantLock();
  
  public ParkingSpot acquireSpot(SpotType type) {
    lock.lock();
    try {
      Queue queue = freeSpots.get(type);
      return queue != null ? queue.poll() : null;
    } finally {
      lock.unlock();
    }
  }
  
  public void releaseSpot(ParkingSpot spot) {
    lock.lock();
    try {
      freeSpots.get(spot.type).offer(spot);
    } finally {
      lock.unlock();
    }
  }
}

The lock is per-floor: parallel parking on different floors doesn't block. For very high concurrency, consider lock-free structures (ConcurrentLinkedQueue), but for a 10-floor parking lot, simple locking is fine.

The main ParkingLot facade

class ParkingLot {
  private final List floors;
  private final SpotAllocator allocator;
  private final FeeCalculator feeCalculator;
  private final PaymentProcessor paymentProcessor;
  private final TicketIssuer ticketIssuer;
  
  public Ticket parkVehicle(Vehicle vehicle) {
    ParkingSpot spot = allocator.findSpot(vehicle, this);
    if (spot == null) throw new LotFullException();
    spot.park(vehicle);
    Ticket ticket = ticketIssuer.issue(vehicle, spot);
    return ticket;
  }
  
  public Receipt exitVehicle(Ticket ticket, PaymentMethod paymentMethod) {
    BigDecimal fee = feeCalculator.calculate(ticket, Instant.now());
    PaymentResult result = paymentProcessor.process(paymentMethod, fee);
    if (!result.isSuccess()) throw new PaymentFailedException();
    ticket.spot.release();
    return new Receipt(ticket, fee, result.getTransactionId());
  }
  
  ParkingSpot findFreeSpot(SpotType type) {
    for (ParkingFloor floor : floors) {
      ParkingSpot spot = floor.acquireSpot(type);
      if (spot != null) return spot;
    }
    return null;
  }
}

Cross-examination round 1: extensibility

Interviewer: A new electric scooter (Vehicle subtype) appears. How do you add support?
Three things to do: (1) Add ELECTRIC_SCOOTER to VehicleType enum (or create ElectricScooter class extending Vehicle). (2) Update SpotAllocator's preferred-types mapping for this new vehicle (probably motorcycle spot + EV spot). (3) Add pricing rate to FeeCalculator. No changes to existing Vehicle, Spot, Payment, or Ticket logic. Single-responsibility means each change is localised.
Follow-up: What if pricing for the new vehicle is time-of-day dependent (off-peak discount)?
A: Refactor FeeCalculator into a PricingStrategy interface with implementations FlatRateStrategy, TimeOfDayStrategy, etc. Inject the strategy at construction. Now we can per-vehicle assign different strategies if needed.

Cross-examination round 2: testing

Interviewer: How do you unit-test the parkVehicle method without a real payment processor?
Dependency injection saves us. PaymentProcessor is a constructor-injected dependency on ParkingLot. In tests, pass a mock or stub PaymentProcessor that returns predictable results. Similarly for SpotAllocator, FeeCalculator, TicketIssuer. The fact that ParkingLot doesn't construct its own dependencies makes it testable.

Cross-examination round 3: edge cases

Interviewer: A user loses their ticket. How do you handle exit?
Define a recovery flow. Either: (a) Lost-ticket fee — charge a fixed punitive amount regardless of duration; user exits. (b) Manual reconciliation — staff queries entry by license plate (assumes we recorded plate at entry); calculates fee from that record. (c) Maximum fee — charge the full-day maximum. Most production parking lots use (a) for simplicity. Implementation: a separate "exitWithLostTicket" flow in the API.

Takeaway

The parking lot is canonical because it hits every LLD primitive: entity identification (Vehicle, Spot, Ticket), inheritance (vehicle/spot types), Strategy pattern (allocator, payment, pricing), Open-Closed (extensible without modifying existing code), and concurrency (thread-safe spot allocation). A senior candidate's design defends each decision with reasoning — why Strategy here, why not just an enum-switch — and demonstrates that they could extend the design tomorrow without breaking existing code.

The deeper lesson: good LLD is about choosing the right level of abstraction for each piece. Vehicle types are an inheritance hierarchy because they have intrinsic behavior differences. Payment methods are a strategy interface because they're interchangeable algorithms. Spots are objects-with-state because they have a lifecycle (free → occupied → free). Each choice matches the problem.