Cruxly › Low-level design › Running the LLD Interview
From Nouns to a Defensible Model
entities, ownership, invariants — in that order
Open the interactive lessonPrompt: you have a clarified scope for a parking lot: multiple levels, three vehicle sizes, ticket on entry, hourly fee on exit, multiple concurrent gates.
Produce the class model. Be ready to defend every class you name — and every one you didn't.
Nouns first, then aggressive cutting
List every noun the clarified prompt contains: lot, level, spot, vehicle, size, ticket, entry, exit, fee, rate, gate.
Now cut, with a reason for each cut you can say out loud.
Gate— an input device. It callslot.admit(vehicle); it is not part of the domain model. Cut, mention that it's I/O.Entry/Exit— not entities, they are events that create and close aTicket. Cut, folded into Ticket's lifecycle.Size— not a class, an enum on bothVehicleandSpot. Cut to a value.Rate— this is pricing, which the scope said will vary. Keep, but as a strategy interface, not a data class.
What survives: Lot, Level, Spot, Vehicle, Ticket, FeeStrategy. Six. Every one of them earns its place, and you can say why in a sentence each.
Cutting out loud is the valuable part. A candidate who lists eleven classes and a candidate who lists six and explains five cuts have produced very different signals from the same starting list.
Ownership and the shape of the model
Ownership is what stops a model from becoming a bag of mutually-referential objects.
:::key Arrow direction is a design decision, so state it Ticket points at Spot. Spot does not hold a list of tickets.
If Spot held its history, every spot would grow without bound — and lookups by ticket would still need a separate index. So the reference goes one way, and a Map<TicketId, Ticket> on the lot handles lookup. :::
A Spot does need to know whether it is currently occupied — but that is one nullable field (currentTicketId), not a collection. That distinction is the whole difference between modelling state and modelling history.
Invariants — the actual design
Write these before any method signature:
| # | Invariant | Why it exists |
|---|---|---|
| 1 | A spot holds at most one open ticket | The core one — assignment exists purely to protect it |
| 2 | A ticket is open or closed, never both, never reopens | Makes the state machine trivially auditable |
| 3 | A closed ticket's fee is immutable | Recomputing a fee after the fact is how billing disputes are born |
| 4 | A vehicle has at most one open ticket in a lot | Prevents double-entry; needs an index to enforce cheaply |
Now the methods fall out, and each one exists to protect a numbered invariant:
Ticket admit(Vehicle v); // protects 1 and 4
Fee release(TicketId t); // protects 2 and 3
:::key Two public methods is the finding, not an accident A design that reduces to a small public surface over well-named invariants is far easier to defend than one with fifteen methods and no stated rules.
It is the same design either way. The difference is describing it in the order that shows you understood why it has that shape. :::
Where the interfaces go, and where they don't
The scope said pricing varies, so pricing gets a seam:
interface FeeStrategy { Fee compute(Ticket t, Instant exitTime); }
interface FitPolicy { boolean fits(VehicleSize v, SpotSize s); }
HourlyFee is the first implementation. When the interviewer says "now weekends are double," you add an implementation instead of editing a method — and you say that out loud, because it is the entire reason the seam exists.
FitPolicy is the second candidate: can a motorcycle take a large spot? That rule will change, so it gets a seam too.
:::warn The most common over-engineering tell in an LLD round Does anything else get an interface? No.
Ticket has one implementation and no expected second. Level likewise. Adding interfaces "for testability" on entities with no variation is the classic tell — so say explicitly that you are not doing it, and why. The restraint is the signal. :::
:::warn And no ParkingLotManager If a class named Manager or Service accumulates all the behaviour, your entities have become structs and you have written a procedure with extra steps.
Lot.admit() belongs on Lot — because Lot owns the levels it searches. :::
The concurrency story
Multiple gates means admit is contended and release mostly is not.
The naive answer — one lock on Lot — serialises every gate in the building. The interviewer will push on it, so get ahead of it with a lock per level: two cars entering different levels never contend, and admit holds the level lock only while scanning for and claiming a spot, releasing it before creating the ticket.
Spot s = level.findFree(size); // A and B both see spot 7
s.occupy(ticket); // both write — invariant 1 is gone
// one critical section: find and claim cannot be separated
synchronized (level) {
Spot s = level.findFree(size);
if (s != null) s.occupy(ticket);
}
// ...or make occupy() a compare-and-set that can fail and re-search
:::key What's actually being graded here Either fix is acceptable — one critical section, or a CAS that can fail and send you back to searching. Saying which one you chose, and why, is the part being assessed.
release needs no lock beyond the spot itself: it touches one spot and one ticket, and the caller already uniquely owns that ticket. :::
:::note If they told you it's single-threaded Don't write locks. Say: "Single-threaded, so no locks. If this became multi-gate, the contended path is find-and-claim in admit, and I'd lock per level."
Same understanding, one sentence, no wasted code. :::
How to say it in the interview
"I model ownership first: a Lot owns Levels, a Level owns Spots. Then the invariant — a spot has at most one open ticket — because that single sentence is what every method is protecting."
Checklist
- Listed the nouns, then cut most of them with a stated reason per cut
- Stated ownership and reference direction explicitly
- Wrote the invariants before writing any method signature
- Kept current state on the entity and history out of it
- Put interfaces exactly where the prompt implied variation — and nowhere else
- Avoided a god-object Manager; behaviour lives on the entity that owns the data
- Named the contended path and the smallest lock that protects it