The design interview
Turn "design a parking lot" into operations, then classes that each own an invariant, and prove the design works by walking one scenario end to end.
"Design a parking lot." "Design a library." "Design an elevator." The question looks open-ended and is not: in 30 minutes an interviewer wants to see you pick a scope, name the operations, produce a handful of classes that each own something, and demonstrate that the design survives one concrete story.
The budget is tight enough to plan: roughly five minutes on scope and operations, five on candidate nouns and verbs, ten on classes and the invariant each holds, five walking a scenario, five on an extension. Skipping the first ten minutes to start drawing boxes is what produces the twelve-class design that does nothing.
Fix the scope by naming operations
Features are unbounded; operations are countable. Write the calls, not the nouns:
park(vehicle) -> ticket, or a refusal when the lot is fullexit(ticket) -> feeavailability(size) -> count
Three or four operations is a design. Ten is a product, and you will not finish.
Then get the scale, because it decides how much machinery is justified. Three levels of 200 spots is 600 spots, with roughly 1,000 arrivals a day: 1,000 ÷ 86,400 ≈ 0.012 arrivals per second, one car every 17 seconds even at a 5× peak.
Say that out loud, because it rules things out: no concurrency story beyond a single lock, no sharding, no cache.
Ask three clarifying questions and stop — vehicle sizes, payment, reservations — then state the scope and move. "Single lot, three sizes, pay on exit, no reservations" is a decision, and interviewers grade decisions.
Nouns as candidates, then cut hard
List the nouns from the problem statement: parking lot, level, spot, vehicle, car, truck, motorcycle, ticket, payment, rate card, gate, entrance, display board, attendant, receipt. Fifteen candidates. The verbs — park, find a spot, occupy, release, issue, price — are the candidate methods, and they decide which nouns survive.
Keep a noun only if it owns state that can be wrong, or a decision that can be made differently. Everything else is a field, a rendering, or out of scope:
- Car, truck, motorcycle differ only in size. No method behaves differently,
so they are a size value on
Vehicle, not three subclasses. Inheritance that adds no behaviour is a taxonomy hobby. - Receipt is a rendering of a ticket. Display board and gate are I/O at the edges, out of scope until asked. Attendant is a user.
- Payment survives only if you said payment was in scope.
Fifteen nouns become five classes, a Vehicle value and a size enum, and that
reduction is much of what is being scored.
Give every class an invariant it owns
A class earns its place by protecting a rule — say the rule when you introduce it:
- Spot — holds at most one vehicle, and only one whose size fits.
- Level — its free count per size equals its unoccupied spots of that size, after every claim and release.
- ParkingLot — every occupied spot has exactly one open ticket, and no vehicle holds two spots.
- Ticket — has a start time, closes once, and closing sets an end and a fee.
- RateCard — the fee is a pure function of a duration. No state, which is why it is safe to keep separate.
The one interesting decision
Allocation is the one real choice here, and the interviewer is waiting for you to notice it.
The straight version scans:
class Level:
def claim(self, size):
for spot in self._spots:
if spot.is_free() and spot.fits(size):
spot.occupy() # Spot enforces its own rule
return spot
return None
class ParkingLot:
def park(self, vehicle, now):
for level in self._levels:
spot = level.claim(vehicle.size)
if spot is not None:
return self._tickets.open(vehicle, spot, now)
return None # full: a result, not an exception
final class ParkingLot {
Optional<Ticket> park(Vehicle v, Instant now) {
for (Level level : levels) {
Optional<Spot> spot = level.claim(v.size());
if (spot.isPresent()) return Optional.of(tickets.open(v, spot.get(), now));
}
return Optional.empty();
}
}
The alternative is a free list per size per level: a stack or queue of free spots, so claiming is a pop and releasing is a push, O(1) instead of a scan.
Name it, then price both. The scan checks 600 spots at roughly 100 ns each ≈ 60 µs per car, against one arrival every 17 seconds. The free list buys nothing measurable and adds a second structure that must stay in sync with the spots, the two-structures-one-invariant problem, which is where the bugs will live.
So take the scan and say why: "the free list is the upgrade when the policy gets
interesting — nearest to the lift, or EV spots last." Keep the decision behind
claim, so an allocator can be swapped without any caller changing — a seam
worth naming, on the terms in
designing to an interface.
Walk one scenario, end to end
A design is a claim, and one traced story is the evidence.
A truck arrives at 09:14. park asks level 0, which has no free large spot,
then level 1, which finds L1-A17, checks the size fits and marks it occupied —
free-large drops from 12 to 11. The lot opens ticket T-902 holding that spot and
09:14. At 12:24 the driver presents T-902: the ticket closes, the rate card
prices 3 h 10 m as a first hour plus three started hours, 50 + 3 × 30 = 140, the
spot is released, and free-large returns to 12. Every invariant is back where it
started.
Then walk the two failures. The lot is full: park returns nothing rather than
throwing, so the caller must handle it. The ticket is lost: that needs a lookup
by plate, a new operation and a scope question — say so instead of inventing
it.
The failure mode: twelve classes that do nothing
The common wreck is a Spot with get_id, is_occupied and set_occupied, a
Ticket with six getters, and a ParkingLotManager holding every rule. That is
a procedure with extra syntax, and nothing stops two vehicles being written into
one spot, because the check lives in whichever caller remembers it.
The test takes seconds per class: what can this class refuse to do? Spot
refuses a second vehicle and a truck in a small bay. Ticket refuses to close
twice. Level refuses to hand out a spot it already gave away. A class that
refuses nothing is data — make it a dataclass or a record and put the rule where
the state lives, which is the judgment the
SOLID refactors turn on.
In an interview
Narrate the cuts, because the cuts are the signal. "Fifteen nouns; I am keeping five. Car and truck differ only by size, so that is a field, not a hierarchy." Then introduce each class by its invariant rather than its fields: "Spot owns one rule, at most one vehicle and the size has to fit".
Finish with the scenario walk even if the clock is tight. A design nobody traced is untested, and the trace is where missing state shows up.
The mistake that loses points is silence during the cutting phase, followed by twelve boxes with no methods. The second-worst is designing for a scale nobody asked for: queues and shards for one car every 17 seconds.
Check yourself
600 spots, 1,000 arrivals a day. Is a linear scan for a free spot acceptable? Show the arithmetic.
1,000 ÷ 86,400 ≈ 0.012 arrivals per second, or one car every 17 seconds at a 5× peak. A 600-spot scan at roughly 100 ns a check is about 60 µs, five orders of magnitude inside that gap. Take the scan, keep it behind a method, and name the free list as the upgrade.
Which of car, truck, motorcycle, receipt and rate card survive as classes?
Rate card survives — it owns a pricing rule. Car, truck and motorcycle collapse into a size value on
Vehicle, because no method behaves differently between them. Receipt is a rendering of a ticket, so it is a method, not a class.
An interviewer points at your Spot, which has only getters and a setter, and
asks what is wrong.
The rule that a spot holds at most one vehicle of a fitting size now lives in every caller that sets the field, so any caller that forgets it corrupts the lot. Move
occupyandreleaseontoSpotso it can refuse.