Parking Lot
The canonical OOD interview problem. Multi-floor lot, spot types, tickets, payment. The first hour of every LLD prep.
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.status—Issued → Paid → Closed(plus aLostbranch with a lost-ticket fee). The class diagram leaves the states implicit; the sequence diagram below makes the transitions explicit. - Strategy pattern on
PaymentStrategy—CashPayment,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
Vehiclecarries only type and plate; fare lookup goes throughRateTable, which is the one place rates change. (SRP — fare logic belongs to the pricing stakeholder, not the vehicle abstraction.) - No
SpotManagergod class. Allocation is delegated toFloor.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:
synchronizedon 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.Clockis injected. Tests can fast-forward without sleeping. If you hardcodeInstant.now(), you have invited an entire category of flaky tests.Moneyis its own type. Even in a slice, do not pass fares asdouble. The Money type encapsulates currency and rounding.fitslives 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 ofinstanceof.
Trade-offs and extensions#
Decisions explicitly made and what they cost:
| Decision | Why | Cost if requirements change |
|---|---|---|
Vehicle as a flat enum, not a class hierarchy | Three 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 pricing | One place to change when ops rewrites rates. SRP. | Per-floor, per-time-of-day, or dynamic pricing needs a richer rate API. |
PaymentStrategy interface | Cash/Card today; UPI tomorrow without editing ParkingLot. | None — it is the right shape now. |
First-fit allocation in Floor.findFreeSpot | O(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 ParkingLot | Correctness first; throughput is well within budget. | At 10⁵+ spots or multi-gate hot lots, partition the lock per floor. |
| In-process state | No 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
Customerentity with a subscription; rate becomes 0 when the ticket’s owner has an active pass. Add aRatePolicystrategy and letRateTableconsult it. - EV spots. Promote
SpotTypeto includeEV_COMPACT/EV_LARGE;Vehiclegains an EV flag;fitsextends. The class diagram does not shift — only the leaf data. - Reservations. A new aggregate (
Reservation) holds a spot until a deadline.findFreeSpotconsults a reservation index. State machine on the reservation:Held → Activated (ticket issued) | Expired. - Multiple gates.
EntryGateandExitGateare 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
ParkingLotknowing they exist. (Observer pattern.) - “What’s the state machine on
Ticket?” —Issued → (Lost?) → Paid → Closed. Show the activity diagram. - “Why not put
fare()onVehicle?” — Pricing is owned by ops, not by the vehicle abstraction. Putting it onVehiclecouples 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 toPaidon settlement, thenClosed. The state machine already covers it. - “Two cars arrive at the same time, one spot left.” — Coarse
synchronizedonparkVehicle; the loser receivesNoSpotAvailableException. 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
ParkingFacilityaggregate; rates may become per-facility; persistence becomes mandatory; the in-process design stays valid per lot.
Related#
- Approaching the OOD Interview — the meta-script that produced this writeup’s structure.
- Elevator System — the next-most-common OOD prompt; shares the State pattern on the moving entity.
- Strategy Pattern —
PaymentStrategyhere is the textbook use. - State Pattern —
Ticketlifecycle is a clean state machine. - Observer Pattern — the answer to the “where would you add observers?” follow-up.