Design questions6 min · 16 of 16

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 full
  • exit(ticket) -> fee
  • availability(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.
Every box owns an invariant. A class that owns none is a field on its neighbour.
Five classes for a parking lot: the lot allocates a spot through a level, the level owns free spots by size, the spot owns its own occupancy, and a ticket priced by a rate card records the staypark(car)ask eachoccupyissuepriceDriverParkingLotowns allocationLevelfree spots by sizeSpotone car or noneTicketspot + start timeRateCardhours to moneyEvery box owns an invariant. A class with none is afield on its neighbour.

Scroll to zoom · drag to pan · 0 fits · Esc closes

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 occupy and release onto Spot so it can refuse.