SDE Path

Walkthrough: Design a Parking Lot

10 min read

Model the domain first

Before writing any behaviour, name the nouns. A parking lot has spots of different sizes, vehicles that fit certain spots, and tickets issued on entry. Start with small, closed value types.

enum VehicleSize { MOTORCYCLE, CAR, TRUCK }

class Vehicle {
  private final String plate;
  private final VehicleSize size;
  Vehicle(String plate, VehicleSize size) { this.plate = plate; this.size = size; }
  VehicleSize getSize() { return size; }
}

class ParkingSpot {
  private final int id;
  private final VehicleSize size;
  private Vehicle vehicle;                 // null when free
  ParkingSpot(int id, VehicleSize size) { this.id = id; this.size = size; }
  VehicleSize size() { return size; }
  boolean isFree() { return vehicle == null; }
  boolean fits(Vehicle v) { return isFree() && v.getSize().ordinal() <= size.ordinal(); }
  void assign(Vehicle v) { this.vehicle = v; }
  void release() { this.vehicle = null; }
}

The vehicle field is private and mutated only through assign/release — no caller can leave a spot half-occupied. Using an enum for size (instead of MotorcycleSpot/CarSpot subclasses) turns "does this vehicle fit?" into a one-line ordinal comparison rather than a type check.

<!--STEP-->

Make spot-finding a strategy

Which free spot should a car get? The nearest? The smallest that fits (to save big spots for trucks)? You may not know yet — and the interviewer might change it. Hide the choice behind an interface so the lot never hard-codes one policy.

interface SpotFinder {
  ParkingSpot find(List<ParkingSpot> spots, Vehicle v);
}

class SmallestFitFinder implements SpotFinder {
  public ParkingSpot find(List<ParkingSpot> spots, Vehicle v) {
    return spots.stream()
      .filter(s -> s.fits(v))
      .min(Comparator.comparingInt(s -> s.size().ordinal()))   // smallest that fits
      .orElse(null);
  }
}

Now switching to a "nearest spot" policy is a new class, not an edit to the lot — the Open/Closed Principle in one move.

<!--STEP-->

Issue a ticket on entry

Parking must capture when a car entered so we can price it later. That belongs on a Ticket, created atomically with the assignment.

class Ticket {
  private final String id;
  private final ParkingSpot spot;
  private final Instant entryAt;
  Ticket(String id, ParkingSpot spot) {
    this.id = id; this.spot = spot; this.entryAt = Instant.now();
  }
  Instant entryAt() { return entryAt; }
  ParkingSpot spot() { return spot; }
}

Recording entryAt at creation — not recomputing it at exit — is the only way to price the actual stay. If you tried to derive entry time on the way out, you would have nothing to derive it from.

<!--STEP-->

Price with a swappable strategy

Flat rate? Hourly? Weekend surge? Pricing changes far more often than parking mechanics. Isolate it behind its own interface.

interface PricingStrategy {
  long feeCents(Ticket t, Instant exitAt);
}

class HourlyPricing implements PricingStrategy {
  private final long ratePerHourCents;
  HourlyPricing(long rate) { this.ratePerHourCents = rate; }
  public long feeCents(Ticket t, Instant exitAt) {
    long hours = Math.max(1, Duration.between(t.entryAt(), exitAt).toHours());
    return hours * ratePerHourCents;
  }
}

When finance asks for surge pricing on Fridays, you add SurgePricing and inject it — ParkingLot doesn't change. Same shape as SpotFinder: behaviour that varies lives behind an interface.

<!--STEP-->

Assemble the lot — and guard the race

class ParkingLot {
  private final List<ParkingSpot> spots;
  private final SpotFinder finder;
  private final PricingStrategy pricing;

  ParkingLot(List<ParkingSpot> spots, SpotFinder f, PricingStrategy p) {
    this.spots = spots; this.finder = f; this.pricing = p;
  }

  synchronized Ticket park(Vehicle v) {            // guard find + assign
    ParkingSpot s = finder.find(spots, v);
    if (s == null) throw new IllegalStateException("Lot full for " + v.getSize());
    s.assign(v);
    return new Ticket(UUID.randomUUID().toString(), s);
  }

  long unpark(Ticket t) {
    t.spot().release();
    return pricing.feeCents(t, Instant.now());
  }
}

park is synchronized because find-then-assign is a check-then-act: two threads could both see the same free spot and both assign it. The lock makes selection and reservation one atomic step. A real multi-entry garage would shrink the critical section (lock per spot / CAS), but the invariant is identical: never hand the same spot to two cars.

Practice it for real: the Parking Lot problem in the LLD ladder gives you a class skeleton and hidden tests that exercise exactly these flows.

Check yourself

5 questions — graded when you submit.

Question 1

Why model spot size as an enum with an ordinal comparison instead of a subclass per size (MotorcycleSpot, CarSpot…)?

Question 2

What does putting spot selection behind the SpotFinder interface buy you?

Question 3

Why capture entryAt on the Ticket at entry rather than computing it at exit?

Question 4

Why is pricing modelled as a PricingStrategy interface injected into the lot?

Question 5

Why is park() declared synchronized?

Finished reading?

Sign in to save your progress across lessons.