Parking Lot

The canonical OOD interview problem. Multi-floor lot, spot types, tickets, payment. The first hour of every LLD prep.

System Foundational
12 min read
ood case-study parking-lot state-pattern strategy-pattern

Context#

A multi-floor parking facility issues tickets to vehicles on entry, assigns each to a compatible spot, and charges a fare on exit based on duration and vehicle class. The problem is the canonical OOD interview opener because it is small enough to design in a 45-minute window, rich enough to surface SOLID and the GoF patterns, and concrete enough that the interviewer can probe edge cases without leaving the domain.

The interviewer’s hidden objectives, in roughly the order they will be tested:

  • Can you clarify scope without spinning out into infinite features?
  • Can you identify the entities — vehicle, spot, ticket, lot, payment — and the relationships between them, especially HAS-A versus IS-A?
  • Can you produce a class diagram that respects SRP, uses inheritance only where IS-A holds, and applies the right patterns where polymorphism beats instanceof?
  • Can you narrate a flow — entry, exit, full lot — at the level of objects sending messages?
  • Can you defend trade-offs when the interviewer pushes (multiple gates, reservations, monthly passes, electric charging spots)?

Requirements (functional and non-functional)#

Clarifying in the room is the most points-bearing part of the round. The scope below is the one most interviewers expect; anything outside it should be flagged as out-of-scope so you can finish.

Functional — in scope.

  • Multi-floor lot with multiple spot types: motorcycle, compact, large. Each vehicle class is compatible with a fixed subset of spot types.
  • Issue a ticket on entry; record entry time and assigned spot.
  • On exit, compute the fare from duration and a rate table; collect payment in cash or by card; release the spot.
  • A vehicle is rejected if no compatible spot is available.
  • Report current availability per floor per spot type.

Functional — out of scope (called out explicitly). Reservations, monthly passes, dynamic pricing, EV charging, valet, automated number-plate recognition. These will not be discussed unless the interviewer adds them as a follow-up.

Non-functional.

  • The lot has on the order of 10⁴ spots. In-process data structures suffice; no database design required.
  • Latency target: entry and exit decisions in under 100 ms (sets a soft bound on how clever the spot-allocation algorithm can be).
  • Concurrency: multiple entry/exit gates may transact simultaneously; the design must avoid double-assigning a spot.

Use case diagram#

┌────────────────┐
│ Driver │
└────────┬───────┘
┌───────────────┼───────────────┐
▼ ▼ ▼
[park vehicle] [retrieve vehicle] [pay fare]
│ │ ▲
│ │ │
▼ ▼ │
┌──────────────────────────────────┐ │
│ Parking Lot System │──┘
└──────────────┬───────────────────┘
┌────────────────────┐
│ System Admin │ … configure rates, add floors, audit revenue
└────────────────────┘

Two primary actors (Driver, Admin) and three primary use cases (park, retrieve, pay). The admin use cases stay implicit — they are out of scope but worth naming so the interviewer knows you saw them.

Class diagram#

┌──────────────────────┐
│ ParkingLot │
├──────────────────────┤
│ floors : List<Floor> │
│ payment : PaymentStrategy │
│ rates : RateTable │
├──────────────────────┤
│ parkVehicle(v) │
│ retrieveVehicle(t) │
│ availabilityReport() │
└──────────┬───────────┘
│ 1..*
┌────────────────┐
│ Floor │
├────────────────┤
│ id │
│ spots: List<ParkingSpot> │
├────────────────┤
│ findFreeSpot(v)│
└────────┬───────┘
│ 1..*
┌────────────────────────┐
│ ParkingSpot │ ◇──── state: SpotState (Free/Occupied/OutOfOrder)
├────────────────────────┤
│ id, type : SpotType │
├────────────────────────┤
│ fits(v) │
│ assign(t : Ticket) │
│ release() │
└────────────────────────┘
┌─────────────────┐ ┌────────────────┐ ┌─────────────────┐
│ Vehicle │ │ Ticket │ │ RateTable │
├─────────────────┤ ├────────────────┤ ├─────────────────┤
│ plate │ │ id │ │ ratePerHour(v) │
│ type:VehicleType│ │ vehicle │ │ fareFor(t,now) │
└────────┬────────┘ │ spot │ └─────────────────┘
│ │ enteredAt │
┌──────────────┼───────┐ │ exitedAt? │ ┌────────────────────┐
▼ ▼ ▼ │ status:State │ │ PaymentStrategy │◁── Cash, Card
Motorcycle Car Truck └────────────────┘ └────────────────────┘

Two patterns are doing the load-bearing work:

  • State pattern on Ticket.statusIssued → Paid → Closed (plus a Lost branch with a lost-ticket fee). The class diagram leaves the states implicit; the sequence diagram below makes the transitions explicit.
  • Strategy pattern on PaymentStrategyCashPayment, CardPayment. The lot does not switch on payment kind.

What is not in the diagram and that is deliberate:

  • Vehicle has no fare logic. The class hierarchy on Vehicle carries only type and plate; fare lookup goes through RateTable, which is the one place rates change. (SRP — fare logic belongs to the pricing stakeholder, not the vehicle abstraction.)
  • No SpotManager god class. Allocation is delegated to Floor.findFreeSpot. The lot iterates floors; the floor iterates its own spots.

Sequence diagram (key flows)#

The park-and-pay flow, in object messages:

Driver EntryGate ParkingLot Floor Spot Ticket
│ park(v) │ │ │ │ │
│───────────────►│ │ │ │ │
│ │ parkVehicle(v) │ │ │ │
│ │───────────────►│ │ │ │
│ │ │ findFreeSpot(v)│ │ │
│ │ │───────────────►│ │ │
│ │ │ │ fits(v)? │ │
│ │ │ │─────────────►│ │
│ │ │ │ yes │ │
│ │ │ │◄─────────────│ │
│ │ │ │ assign(t) │ │
│ │ │ │─────────────►│ │
│ │ │ Ticket t │ │ │
│ │ │◄──────────────────────────────────new()─────│
│ │ ticket │ │ │ │
│ │◄───────────────│ │ │ │
│ ticket │ │ │ │ │
│◄───────────────│ │ │ │ │

The exit flow adds the fare computation and state transition:

Driver ExitGate ParkingLot RateTable PaymentStrategy Ticket
│ retrieve(t) │ │ │ │ │
│──────────────►│ │ │ │ │
│ │ retrieve(t) │ │ │ │
│ │───────────────►│ │ │ │
│ │ │ fareFor(t,now) │ │ │
│ │ │───────────────►│ │ │
│ │ │ fare │ │ │
│ │ │◄───────────────│ │ │
│ │ │ pay(fare) │ │
│ │ │──────────────────────────────────►│ │
│ │ │ receipt │ │
│ │ │◄──────────────────────────────────│ │
│ │ │ markPaid() ──────────────────────────────────────────►│
│ │ │ release() │ │
│ │ │── spot.release() ── floor ── spot ─┘ │

Activity diagram (for non-trivial state)#

The Ticket lifecycle is where most subtle bugs live; an activity diagram makes the legal transitions explicit:

┌─────────┐
│ start │
└────┬────┘
┌────────────┐
┌───────►│ Issued │──── exit attempted ────► [fare > 0?]
│ └────────────┘ │
│ │ yes │ no
│ │ lost-ticket flag │ │
│ ▼ ▼ ▼
│ ┌────────────┐ ┌────────────┐
│ │ Lost │── lost-fee paid ────► │ Paid │
│ └────────────┘ └─────┬──────┘
│ │
│ ▼
│ spot released
│ │
│ ▼
│ ┌────────────┐
└────────── (re-entry — new ticket) ─────────│ Closed │
└────────────┘

Closed is terminal; re-entry produces a brand-new Ticket. Lost → Paid exists; Issued → Closed without Paid does not.

Java implementation#

A representative slice of the design, not the full system. It shows the load-bearing decisions; the rest is mechanical.

public enum VehicleType { MOTORCYCLE, CAR, TRUCK }
public enum SpotType { MOTORCYCLE, COMPACT, LARGE }
public final class Vehicle {
private final String plate;
private final VehicleType type;
public Vehicle(String plate, VehicleType type) { this.plate = plate; this.type = type; }
public VehicleType type() { return type; }
public String plate() { return plate; }
}
public final class ParkingSpot {
private final String id;
private final SpotType type;
private Ticket occupiedBy; // null when free
public ParkingSpot(String id, SpotType type) { this.id = id; this.type = type; }
public boolean isFree() { return occupiedBy == null; }
public boolean fits(Vehicle v) {
return switch (v.type()) {
case MOTORCYCLE -> type == SpotType.MOTORCYCLE || type == SpotType.COMPACT || type == SpotType.LARGE;
case CAR -> type == SpotType.COMPACT || type == SpotType.LARGE;
case TRUCK -> type == SpotType.LARGE;
};
}
public synchronized void assign(Ticket t) {
if (!isFree()) throw new IllegalStateException("Spot " + id + " is already occupied");
this.occupiedBy = t;
}
public synchronized void release() {
this.occupiedBy = null;
}
}
public final class Ticket {
public enum Status { ISSUED, LOST, PAID, CLOSED }
private final String id;
private final Vehicle vehicle;
private final ParkingSpot spot;
private final Instant enteredAt;
private Instant exitedAt;
private Status status = Status.ISSUED;
public Ticket(String id, Vehicle v, ParkingSpot s, Instant enteredAt) {
this.id = id; this.vehicle = v; this.spot = s; this.enteredAt = enteredAt;
}
public void markPaid(Instant exitedAt) {
if (status != Status.ISSUED && status != Status.LOST) {
throw new IllegalStateException("Cannot pay a ticket in state " + status);
}
this.exitedAt = exitedAt;
this.status = Status.PAID;
}
public void close() {
if (status != Status.PAID) throw new IllegalStateException("Cannot close unpaid ticket");
this.status = Status.CLOSED;
}
public Vehicle vehicle() { return vehicle; }
public ParkingSpot spot() { return spot; }
public Instant enteredAt() { return enteredAt; }
public Status status() { return status; }
}
public interface PaymentStrategy {
Receipt pay(Money amount);
}
public final class RateTable {
public Money fareFor(Ticket t, Instant now) {
long minutes = Duration.between(t.enteredAt(), now).toMinutes();
double hours = Math.max(1, Math.ceil(minutes / 60.0));
double perHour = switch (t.vehicle().type()) {
case MOTORCYCLE -> 20.0;
case CAR -> 50.0;
case TRUCK -> 100.0;
};
return Money.rupees(hours * perHour);
}
}
public final class Floor {
private final int id;
private final List<ParkingSpot> spots;
public Floor(int id, List<ParkingSpot> spots) { this.id = id; this.spots = spots; }
public Optional<ParkingSpot> findFreeSpot(Vehicle v) {
for (ParkingSpot s : spots) {
if (s.isFree() && s.fits(v)) return Optional.of(s);
}
return Optional.empty();
}
}
public final class ParkingLot {
private final List<Floor> floors;
private final RateTable rates;
private final PaymentStrategy payment;
private final Clock clock;
public ParkingLot(List<Floor> floors, RateTable rates, PaymentStrategy payment, Clock clock) {
this.floors = floors; this.rates = rates; this.payment = payment; this.clock = clock;
}
public synchronized Ticket parkVehicle(Vehicle v) {
for (Floor f : floors) {
Optional<ParkingSpot> s = f.findFreeSpot(v);
if (s.isPresent()) {
Ticket t = new Ticket(UUID.randomUUID().toString(), v, s.get(), clock.instant());
s.get().assign(t);
return t;
}
}
throw new NoSpotAvailableException("Lot is full for vehicle type " + v.type());
}
public synchronized Receipt retrieveVehicle(Ticket t) {
Money fare = rates.fareFor(t, clock.instant());
Receipt r = payment.pay(fare);
t.markPaid(clock.instant());
t.spot().release();
t.close();
return r;
}
}

Notes the interviewer will look for:

  • synchronized on the lot’s mutating methods. The non-functional concurrency requirement is satisfied by a coarse lock — fine enough for ~10⁴ spots and entry rates measured in vehicles per minute. If they push for higher throughput, the answer is per-floor or per-section locking, not lock-free trickery.
  • Clock is injected. Tests can fast-forward without sleeping. If you hardcode Instant.now(), you have invited an entire category of flaky tests.
  • Money is its own type. Even in a slice, do not pass fares as double. The Money type encapsulates currency and rounding.
  • fits lives on the spot, not on a switch elsewhere. Compatibility is a property of the spot’s relationship to the vehicle; co-locating it there is what keeps the rest of the code free of instanceof.

Trade-offs and extensions#

Decisions explicitly made and what they cost:

DecisionWhyCost if requirements change
Vehicle as a flat enum, not a class hierarchyThree types, no per-type behaviour beyond fare. Composition over inheritance.If a vehicle type later needs distinct behaviour (e.g. EVs need charging), promote to a class hierarchy with strategies.
RateTable centralises pricingOne place to change when ops rewrites rates. SRP.Per-floor, per-time-of-day, or dynamic pricing needs a richer rate API.
PaymentStrategy interfaceCash/Card today; UPI tomorrow without editing ParkingLot.None — it is the right shape now.
First-fit allocation in Floor.findFreeSpotO(n) is fine at 10⁴ spots and 1 vehicle/sec. Simple to reason about.If you must minimise walking distance, switch to a priority queue per spot type keyed by proximity to entrance.
Single coarse lock on ParkingLotCorrectness first; throughput is well within budget.At 10⁵+ spots or multi-gate hot lots, partition the lock per floor.
In-process stateNo persistence requirement was given.Adding a database means introducing a repository per aggregate (TicketRepository, SpotRepository) — the interfaces are clean places to do it.

Likely follow-up extensions and the shape of the answer:

  • Monthly passes. A new Customer entity with a subscription; rate becomes 0 when the ticket’s owner has an active pass. Add a RatePolicy strategy and let RateTable consult it.
  • EV spots. Promote SpotType to include EV_COMPACT/EV_LARGE; Vehicle gains an EV flag; fits extends. The class diagram does not shift — only the leaf data.
  • Reservations. A new aggregate (Reservation) holds a spot until a deadline. findFreeSpot consults a reservation index. State machine on the reservation: Held → Activated (ticket issued) | Expired.
  • Multiple gates. EntryGate and ExitGate are already separate; multiplying them is a deployment concern, not a design one — provided the lock granularity holds up.

Mock interview follow-ups#

Questions interviewers reach for and the briefest correct answer:

  • “Where would you add observers?” — On exit-gate revenue events, so analytics and the floor display can subscribe without ParkingLot knowing they exist. (Observer pattern.)
  • “What’s the state machine on Ticket?”Issued → (Lost?) → Paid → Closed. Show the activity diagram.
  • “Why not put fare() on Vehicle?” — Pricing is owned by ops, not by the vehicle abstraction. Putting it on Vehicle couples a hot policy to a cold class and violates SRP.
  • “How do you handle a lost ticket?” — Flag the ticket Lost; charge a fixed lost-ticket fee plus the max-day fare; transition to Paid on settlement, then Closed. The state machine already covers it.
  • “Two cars arrive at the same time, one spot left.” — Coarse synchronized on parkVehicle; the loser receives NoSpotAvailableException. If the interviewer pushes on contention, partition the lock per floor and accept brief unfair losses at the floor boundary.
  • “What changes if it’s a chain of 50 lots?” — Push concerns out: introduce a ParkingFacility aggregate; rates may become per-facility; persistence becomes mandatory; the in-process design stays valid per lot.
Search ESC

Keyboard shortcuts

Shortcuts are disabled while typing in inputs.