Build a Parking Lot
Build a Parking Lot
The parking lot is the canonical LLD interview opener — and most candidates stall the moment "model it" turns into "make two cars not grab the same spot under load." You already understand the class breakdown from the Design a Parking Lot walkthrough; this project makes you build it from an empty file in Go and Java, with O(1) allocation and a concurrency test that fails loudly when you forget the lock. That gap — from "I can draw the classes" to "I shipped the allocator and debugged its race" — is what the interviewer is actually probing.
The spec
Build a lot with this contract, then grow it in milestones:
- Core:
park(vehicle) → Ticket | null(null when full) andunpark(ticket) → fee. - Fit rules: spots have sizes (
MOTORCYCLE < COMPACT < LARGE); a vehicle takes the smallest spot it fits in, and may upgrade to a bigger free spot if its size is full. - O(1) allocation: finding a free spot must NOT scan every spot — keep a free-list per size.
- Thread-safe: correct under concurrent
park/unpark— two cars must never receive the same spot. This is where the real bug lives.
Milestones
- M1 — One level, one size.
park/unparkwith a stack of free spots. Single-threaded; get the lifecycle right. - M2 — Sizes + fit + upgrade. A free-list per
SpotType; smallest-fit, then upgrade. Reject only when nothing fits. - M3 — Multi-level + "nearest". Levels ordered by distance; allocate from the nearest level with a free spot.
- M4 — Thread-safe. Guard the read-modify-write of the free-lists with one lock; fee = f(duration, type). Prove correctness with a concurrent test.
- M5 — EV charging spots. An
ELECTRICspot type with a charging-session lifecycle and an idle fee once the battery is full (discourage squatting after charging completes). - M6 — Dynamic / peak pricing. A pluggable
FareStrategy(flat / tiered / occupancy-surge) so peak-demand pricing swaps without touching the allocator.
Reference implementation — the allocator (Go)
The whole trick is the per-size free-list: allocation and release are O(1) stack ops, never a scan. One mutex makes the pop/push atomic so two goroutines can't pop the same spot.
type SpotType int
const (Motorcycle SpotType = iota; Compact; Large)
type Lot struct {
mu sync.Mutex
free map[SpotType][]int // size → stack of free spot IDs
}
// smallest-fit order, then upgrade to a larger free size.
var fitOrder = map[SpotType][]SpotType{
Motorcycle: {Motorcycle, Compact, Large},
Compact: {Compact, Large},
Large: {Large},
}
func (l *Lot) Park(v SpotType) (spot int, ok bool) {
l.mu.Lock(); defer l.mu.Unlock()
for _, s := range fitOrder[v] {
if n := len(l.free[s]); n > 0 {
spot = l.free[s][n-1]
l.free[s] = l.free[s][:n-1] // pop — O(1)
return spot, true
}
}
return 0, false // full for this vehicle
}
func (l *Lot) Unpark(spot int, s SpotType) {
l.mu.Lock(); defer l.mu.Unlock()
l.free[s] = append(l.free[s], spot) // push — O(1)
}
Reference implementation — the allocator (Java)
enum SpotType { MOTORCYCLE, COMPACT, LARGE }
final class Lot {
private final Map<SpotType, Deque<Integer>> free = new EnumMap<>(SpotType.class);
private static final Map<SpotType, List<SpotType>> FIT = Map.of(
SpotType.MOTORCYCLE, List.of(SpotType.MOTORCYCLE, SpotType.COMPACT, SpotType.LARGE),
SpotType.COMPACT, List.of(SpotType.COMPACT, SpotType.LARGE),
SpotType.LARGE, List.of(SpotType.LARGE));
synchronized Integer park(SpotType v) { // null when full
for (SpotType s : FIT.get(v)) {
Deque<Integer> d = free.get(s);
if (d != null && !d.isEmpty()) return d.pop(); // O(1)
}
return null;
}
synchronized void unpark(int spot, SpotType s) {
free.get(s).push(spot); // O(1)
}
}
Note the deliberate order in park: hold ONE lock across "find a free spot AND remove it." Splitting find from remove is the classic bug — see the test below.
Tests to write (this is the real learning)
- Capacity: a level with 2 compact spots — 3 compact cars → 2 tickets + 1 null. Proves rejection.
- Upgrade: 0 motorcycle spots, 1 compact free → a motorcycle parks in the compact spot; then a compact car is rejected. Proves smallest-fit-then-upgrade and its cost.
- Release: unpark one, then park again succeeds in that exact spot. Proves the free-list round-trips.
- Concurrency (the lesson): 1 free spot, launch 100 goroutines/threads all calling
park→ assert exactly one succeeds and no spot ID is handed out twice. Run Go with-race. Now remove the lock (or split find/remove into two critical sections) and watch two threads pop the same ID — that failure is the whole point of the exercise.
Real-world scenarios — EV charging & dynamic pricing (the common follow-up)
The most-asked "make it real" escalation adds two things a modern lot has: EV charging spots and peak/occupancy pricing. Interview caution (worth saying out loud): don't volunteer a complex pricing engine unless asked — propose it as an extension, then build it if they bite. The key design move is that pricing is a strategy, not an if-else in the exit gate: the allocator never changes.
- EV spot type + session: add
ELECTRICto the size enum (it's a capability, so a non-EV car may still park there if you allow it — a policy decision to defend). The spot carries a charging session; once the battery is full the meter switches from parking rate to a punitive idle fee so a charged car doesn't squat a scarce charger. - Dynamic pricing via a strategy: the fee is computed by a swappable
FareStrategy— flat, time-tiered, or occupancy-surge (price rises as the lot fills), directly analogous to ride-hailing surge.
// Go — pricing is a strategy; the allocator never sees it.
type FareStrategy interface { Fee(d time.Duration, t SpotType, occupancy float64) float64 }
type SurgeFare struct{ base, ratePerHr float64 }
func (s SurgeFare) Fee(d time.Duration, t SpotType, occ float64) float64 {
mult := 1.0
if occ > 0.9 { mult = 2.0 } else if occ > 0.75 { mult = 1.5 } // occupancy surge
return (s.base + s.ratePerHr*d.Hours()) * mult
}
// Java — same shape; inject the strategy, swap at runtime.
interface FareStrategy { double fee(Duration d, SpotType t, double occupancy); }
final class SurgeFare implements FareStrategy {
private final double base, ratePerHr;
SurgeFare(double base, double ratePerHr){ this.base=base; this.ratePerHr=ratePerHr; }
public double fee(Duration d, SpotType t, double occ) {
double mult = occ > 0.9 ? 2.0 : occ > 0.75 ? 1.5 : 1.0; // occupancy surge
return (base + ratePerHr * d.toMinutes()/60.0) * mult;
}
}
Test it: at occupancy 0.95 the fee is 2× the base rate; drop below 0.75 and it returns to 1×. And confirm the idle fee kicks in only after a charging session reports full — a car still charging pays the parking rate, not the idle penalty.
Extensions — take it further
- Fit policy is a decision, not a given: should a motorcycle be allowed to burn a LARGE spot? Make the upgrade policy pluggable and defend it (utilization vs. starving large vehicles).
- Reservations as time-bound slots (a
Reservationwith a hold + a background no-show reaper) — needs a strongly-consistent store to avoid double-booking, unlike the eventually-consistent availability count. - Sharded locks: one mutex per level instead of one global — cuts contention; now reason about the "nearest across levels" query spanning shards.
- Distributed multi-garage: a central availability service — and the same double-allocation race reappears as a distributed one (compare-and-set / lease).
What you've mastered when this is done
- You can model an entity system with enums + composition and justify the fit/upgrade and pricing policies as pluggable decisions, not hard-codes.
- You allocate in O(1) with per-size free-lists instead of scanning — and can say why the scan is the naive trap.
- You've written — and broken — the concurrency test, so you can explain the double-allocation race a shared-resource allocator creates and exactly which lock scope fixes it.
- You can extend a single-lot allocator to sharded and distributed forms and name the new failure mode (the race becomes a distributed compare-and-set).
Re-authored/Deepened for this guide. Model answer: the Design a Parking Lot LLD walkthrough.
🤖 Don't fully get this? Learn it with Claude
Stuck on Build a Parking Lot? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.
Build the mental picture, not memorization.
I just read a lesson on **Build a Parking Lot** (Hands-On Builds) and want to truly understand it. Explain Build a Parking Lot from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Socratic — adapts to where you're stuck.
Teach me **Build a Parking Lot** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Active recall exposes what you missed.
Quiz me on **Build a Parking Lot** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Intuition + hook + flashcards for long-term memory.
Help me remember **Build a Parking Lot** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.