The idea
What it is
The interviewer says one sentence — “Design a parking lot.” — and starts a timer. That is the whole prompt. It is deliberately tiny, because the question is not really “can you park a car?”. It is “when I hand you a vague problem, what do you do in the first ten minutes?”
The good news: a parking lot is a system you already understand. You have driven into one. You took a ticket, you parked, you paid on the way out. Everything you need to model is a thing you can point at in the real world — and that is exactly why this problem is the warm-up everybody starts with.
The whole system in three sentences
A ParkingLot owns floors, and floors own spots. park(vehicle) finds a free spot the vehicle fits into, marks it taken, and hands back a Ticket stamped with the entry time. unpark(ticket) reads that timestamp, charges for the time, and frees the spot. Everything else in this lesson is detail hanging off those three sentences.
What is actually being graded
Interviewers do not score you on how many classes you produced. Almost every scorecard for this round looks like the same five lines:
- Does it run? A
main()that parks three vehicles and prints two bills beats a beautiful half-finished class hierarchy. Every time. - Are the names the real-world names?
ParkingSpot,Ticket,Vehicle. NotSpotDataManager, notTicketVO. - Can I change a rule without editing your code? This is the big one. They will say “now charge differently on weekends”, and they are watching whether you open an existing method or add a new class.
- Did you handle the ugly cases? Lot full. Truck when only bike spots are free. Ticket presented twice. Two cars arriving at once.
- Did you talk while you worked? Silence reads as “stuck”, even when you are not.
This lesson is the worked example of the framework
The five steps come from A repeatable 5-step framework — clarify → entities → API → class diagram → code. Here we actually walk them, end to end, on a real problem. If a step feels rushed, that lesson explains the why; this one shows the what.
Mechanics
How it works
Step 1 · Clarify — 5 minutes, and it is the cheapest time you will spend
You cannot build “a parking lot”. You can build this parking lot. So shrink the problem until it has edges. Four or five short questions is enough — and you say the answers out loud so they become an agreement, not a guess.
- How big is it? — “Two floors, about 16 spots?” If they say “assume small”, a plain in-memory list is fine and you just saved yourself an hour of indexing.
- What can drive in? — “Bikes, cars, trucks?” Three types means three sizes, which means a spot has a size too.
- How is it charged? — “Per hour, paid at exit?” Now you know the ticket needs an entry timestamp. That one answer designs a whole class.
- What happens when it is full? — “Turn the vehicle away.” An answer here stops you from crashing on the demo.
- What is not in scope? — payments, login, a UI, a database. The most valuable question in the round is the one that removes work.
The two questions that eat your clock
“Which database?” and “Should there be a UI?” In a 60-minute round the answers are always in-memory and a main() method. State those as assumptions in one sentence and move. Every minute in a rabbit hole is a minute not spent on code that runs.
Step 2 · Nouns become classes, verbs become methods
Read your clarified problem back and underline the nouns. A noun that has its own data and its own behaviour is a class. Then underline the verbs — those are the methods, and they nearly always belong to the noun that owns the data they touch.
ParkingLot— the front door of the system. It owns the floors and it is the only class the outside world talks to.ParkingFloor— a list of spots. In a small lot you can skip this and let the lot hold spots directly; say so out loud, and add it back the moment they mention multiple floors.ParkingSpot— has an id, a size, and either a vehicle in it or nothing. It knows two things: am I free? and does this vehicle fit?Vehicle— abstract, withBike,Car,Truckunder it. Each one answers what size spot do I need?Ticket— the contract between the lot and the driver: which vehicle, which spot, entry time. Once created, nothing on it changes except its status.SpotAllocationStrategyandPricingStrategy— the two rules that the interviewer is most likely to change on you. Interfaces, notifstatements.
Use enums for the fixed vocabularies
VehicleType { BIKE, CAR, TRUCK }, SpotSize { SMALL, MEDIUM, LARGE }, TicketStatus { ACTIVE, PAID, LOST }. Three enums, thirty seconds, and every stringly-typed bug in this problem disappears. More on why in Enums & constants.
Step 3 · Write the API before you write the code
Before a single field, write the method signatures a caller would use. Names, parameters, return types — nothing else. If you can describe the whole system in four lines here, your model is right. If you cannot, you are still confused and it is far cheaper to find that out now.
lot.park(vehicle) -> Ticket // throws when the lot is full
lot.unpark(ticketId, exitAt) -> Money // the bill; frees the spot
lot.availableSpots(size) -> int // for the display board
lot.setPricing(strategy) -> void // swap the rule, not the codeThree things worth defending out loud, because interviewers ask about all three:
parkreturns aTicket, not aboolean. The caller needs something to bring back later. A boolean tells the driver nothing.unparktakes a ticket id, not the ticket object. A real gate scans a barcode. Taking an id also means the lot decides whether that ticket is still open — the caller cannot lie to it.- Time is a parameter, not
now()read inside.unpark(id, exitAt)is testable in one line.unpark(id)that calls the clock internally is not — you would have to sleep for an hour to test a two-hour bill. This is the single easiest senior-signal to give in this round.
Money is not a double
0.1 + 0.2 is not 0.3 in floating point, and a parking bill that is off by a paisa is a bug you will not enjoy explaining. Use a whole-number minor unit (paise/cents) as long, or BigDecimal. Saying this out loud costs five seconds and lands well. See Immutability & value objects for why Money deserves to be its own tiny type.
Step 4 · The class diagram — draw it, do not describe it
Two small diagrams beat one crowded one. The first is the core model: what holds what. Read the diamonds as “owns” — a lot owns its floors, a floor owns its spots, and if the lot is demolished they go with it.
entryAt — the entire billing system is downstream of that timestamp. Notation refresher: Class diagrams.The second diagram is the part that wins the round. Both rules the interviewer is likely to change — which spot do we give out? and how much do we charge? — hang off the lot as interfaces, with the real rules as small classes underneath.
unpark(). That is Open/Closed (OCP) made concrete — and in the prototype above, the 🎯/📊 and ⏱/🪜 chips are exactly these four boxes being swapped at runtime.Step 5 · The two flows, message by message
There are only two stories in this system. Walk both out loud while you draw them — this is where an interviewer decides whether you actually understand your own diagram.
park() — five messages, and only the lot talks to more than one collaborator. If a class in your diagram is not on this line, ask yourself whether it earns its place. Notation: Sequence diagrams.unpark() — price it, free the spot, close the ticket, hand back the amount. Order matters: close the ticket before you release the spot in your head, so a ticket can never be billed twice.Choosing a spot: the rule everyone gets half-right
A bike fits in a bike spot. It also physically fits in a truck spot. So the naive rule — “find any free spot big enough” — is correct and quietly terrible: park three bikes and your two truck spots are gone. The rule you want is the smallest free spot that fits.
spot.size >= vehicle.requiredSize to filter, then take the minimum size among what is left. In the prototype, the blue spots are the green cells and the orange spots are the orange cell.Once the size filter has run you still have a choice among equals, and that choice is the strategy. Two that are easy to defend:
- Nearest first — lowest floor, lowest spot number. Shortest walk for the driver. Floor 1 fills completely before Floor 2 gets a car.
- Spread out — send each arrival to the emptiest floor. Slower ramps stay clear, and no single floor jams at rush hour.
- Cheapest / reserved / EV-only — the same interface, more classes. This is why
findSpottakes the spot list as a parameter instead of reaching inside the lot: the strategy stays a small, testable, stateless object.
The real reason to use a strategy here
It is not that you expect to ship SpreadOut. It is that when the interviewer says “what if we wanted to fill the top floor first?”, your answer is “one new class, nothing else changes” — and you can say it in four seconds instead of scrolling through an if/else chain looking for where to wedge it in.
Pricing: the same ticket, two different bills
The fee is a pure function of the ticket and the exit time. Nothing else. Keep it that way and pricing becomes trivially testable — and the interviewer's favourite follow-up becomes a one-liner.
unpark() never knew which of these ran. It called pricing.fee(ticket, exitAt) and printed the number. Toggle ⏱ Flat / 🪜 Slabs in the prototype and watch the open tickets re-price live.- Round up to a started hour. 61 minutes is 2 hours. Say this out loud — it is the kind of detail that separates “I thought about it” from “I divided by 3600”.
- Charge a minimum. Somebody who leaves after 4 minutes still pays for one hour, or your lot is free for anyone doing a drop-off.
- Never bill from the current clock. Bill from
ticket.entryAtto theexitAtyou were handed. That is what makes the whole thing unit-testable.
The ticket has a life, and it can only move forwards
A ticket is issued, then paid, then closed. It never goes backwards. Modelling that as an explicit status — instead of inferring it from “is the spot empty?” — is what stops a ticket being billed twice.
unpark on a ticket that is not ACTIVE throws. Notation: State diagrams.Two cars, one spot — the bug nobody mentions until they do
A parking lot has several entry gates, and gates do not take turns. Two threads can read “F1-03 is free” in the same instant, and both can then write themselves into it. You just gave one spot to two cars, and the second one is going to be upset.
// ParkingSpot — the check and the take cannot be split apart
public synchronized boolean tryAssign(Vehicle v) {
if (vehicle != null) return false; // someone got here first
vehicle = v;
return true;
}
// ParkingLot — a loser just asks the strategy again
public Ticket park(Vehicle v) {
for (int attempt = 0; attempt < MAX_RETRIES; attempt++) {
ParkingSpot spot = allocation.findSpot(spots, v)
.orElseThrow(() -> new LotFullException(v.getType()));
if (spot.tryAssign(v)) return issueTicket(v, spot);
}
throw new LotFullException(v.getType()); // heavy contention, give up cleanly
}Do not put synchronized on park() and call it done
It is correct, and it means one car enters the lot at a time across every gate in the building. Lock the spot, not the lot: contention drops to the handful of cars actually competing for the same space. If they push further, mention an AtomicReference<Vehicle> with compare-and-set — same idea, no lock at all. Background: Locks, Mutex, Semaphore, Atomic operations & CAS and Deadlock, race conditions, starvation.
How much of this to actually build
In a 60-minute round, write tryAssign as synchronized and say the rest out loud. That is four extra characters of code and a 20-second explanation, and it reliably reads as senior. Building a full lock-free allocator is how you run out of time.
The extensibility test — where they will actually push you
With ten minutes left, the interviewer stops asking about parking and starts asking about change. This is the real exam. Each of these should be answerable in one sentence, and the sentence should contain the words “a new class”.
- “Charge differently on weekends.” →
class WeekendRate implements PricingStrategy. Wire it inmain(). Nothing else is touched. - “Fill the top floor first.” →
class TopFloorFirst implements SpotAllocationStrategy. Same story. - “Add electric-vehicle spots with a charger.” → a new
SpotFeature(or anELECTRICspot type) plus anEvOnlystrategy. One edit, because the size-fit rule now has a second dimension — say that honestly rather than pretending it is free. - “Two entrance gates.” → no new classes; this is the concurrency answer above.
tryAssignis already atomic, so you are done. - “Show free spots on a board at the entrance.” → a
DisplayBoardthat the lot notifies on every park and unpark. That is Observer, and mentioning it by name costs you nothing. - “Monthly pass holders skip the ticket.” → a
ParkingPassand a second entry path. Be careful here: this is the one follow-up big enough to change your model, so scope it out loud before you touch anything.
Spending the 60 minutes
- Enums and
Vehiclefirst (3 min) — tiny, unblocks everything else. ParkingSpotwithfitsandtryAssign(5 min) — the heart of the model.ParkingLot.park()with a hardcoded nearest-first strategy (8 min) — get a ticket printing before you make anything pluggable.Ticket+unpark()with the simplest pricing (8 min) — now the loop is closed and you have a working system.- Extract the two strategies (5 min) — only now. Extraction is a five-minute refactor when the thing works, and a rabbit hole when it does not.
main()demo (5 min) — park a bike, a car and a truck, print two bills, try to park into a full lot. Print it. Run it.
At minute 45, freeze the feature list
Whatever is unbuilt at 45 stays unbuilt. Spend the last quarter making the demo run and printing clean output. A working lot with one pricing rule scores far above a half-typed lot with four.
Five ways this round is usually lost
- Typing at minute zero. Twenty minutes later you discover the ticket needed an entry time and the rewrite eats the round.
- A god
ParkingLotthat finds spots, prices tickets, prints receipts and manages gates. Ask of each method: is this the lot's job? See Single Responsibility (SRP). - Anaemic classes.
ParkingSpotwith nothing but getters and setters, whileParkingLotreaches in and mutates it. Behaviour belongs next to the data it touches — that is Tell, Don't Ask. - Pattern cosplay. A
ParkingLotFactoryProviderImplbefore there is a single workingpark(). Two strategies is the right amount of pattern for this problem; see Pattern overuse & anti-patterns. - No demo. If it does not run, most interviewers cannot give you the top band no matter how pretty the diagram was.
Interactive prototype
See it. Build it. Break it.
A sandboxed, hands-on simulation — no setup, no install. Play with it as you read.
About this simulation
A working parking lot, not a slideshow. Pick a vehicle and press ▶ Park — the lot answers in four beats: the vehicle arrives, every spot that fits lights up blue, the smallest fitting size narrows it down to orange, and the strategy picks one. A ticket drops into the tray with the entry time on it. The mono line at the top always shows the real method call behind what you just did. Then change the rules: 🎯 Nearest vs 📊 Spread re-routes cars without touching park(), ⏱ Flat vs 🪜 Slabs re-prices without touching unpark(). Push ⏩ +1 hour and watch every open bill grow. Push ⚡ Fill the lot, then Park — and watch the barrier stay down.
Hands-on
Try these yourself
Open the prototype above, predict what happens, then verify.
Watch the four beats of one park
Pick 🚗 Car and press ▶ Park. Do not skip the middle: beat 2 lights up every spot big enough in blue, beat 3 narrows to the smallest fitting size in orange, and only then does the strategy pick one. Now do the same with 🏍 Bike — notice how many more spots turn blue, and that it still takes an S.
Change the strategy, not the code
Park four cars on 🎯 Nearest and watch Floor 1 fill left to right. Press ↺ Reset, switch to 📊 Spread, park four cars again — they alternate floors. Read the mono line at the top while you do it: park() is called identically both times. The only thing that changed is which object the lot is holding.
Make the clock cost money
Park a car, then press ⏩ +1 hour three times. The open ticket re-prices itself on every press. Now switch ⏱ Flat → 🪜 Slabs and watch the same ticket land on a different number — ₹60 becomes ₹65 — with nothing else touched. That is one interface with two implementations.
Fill it up and get refused
Press ⚡ Fill the lot, then select 🚚 Truck and press ▶ Park. The barrier stays down and the entrance shakes. Ask yourself what your code should do here: return null, return an Optional, or throw? Say your answer out loud — the interviewer will ask.
Find the smallest-fit trade-off
Reset, then park three trucks first, then try a car and a bike. Compare with the reverse order. Smallest-fit protects the big spots, but it cannot save you once trucks have taken them. This is the exact conversation to have when they ask “what if the lot is 80% full?”
Now build it yourself
Open a blank file and write it from memory in this order: enums → Vehicle → ParkingSpot → park() → Ticket → unpark() → main(). Time yourself. Then add WeekendRate implements PricingStrategy and confirm you changed zero existing lines to wire it in. If you had to edit something, your interface is in the wrong place.
In practice
When to use it — and what trips people up
The follow-ups, and a one-sentence answer for each
You will be asked at least three of these. Rehearse the answers until they are boring — the calm, fast answer is the one that scores.
- “How do you handle 5,000 spots?” — Keep a free-list per
(floor, size)instead of scanning every spot. Allocation drops from O(n) to O(1). Say it; only build it if there is time. - “Multiple entry and exit gates?” — Gates are just callers of the same lot. The interesting part is concurrency, and
tryAssignalready handles it. - “What if someone loses their ticket?” —
TicketStatus.LOSTplus a flat penalty rate. It is a newPricingStrategy, which is a nice thing to be able to say. - “Reserve a spot in advance?” — Reservation is a third state on the spot (
FREE / RESERVED / OCCUPIED) with an expiry. Warn them this changes the model — the spot is no longer a simple free/taken boolean. - “Different rates per floor?” — The pricing strategy already receives the ticket, and the ticket knows its spot, which knows its floor. Zero model change.
- “Would you persist this?” — A
ParkingLotRepositorybehind an interface, in-memory today, SQL tomorrow. See Repository — naming it takes four seconds and shows you know where the seam goes.
Where this design would genuinely stop working
- Many lots in many cities. The single in-memory
ParkingLotbecomes a service per lot with its own store. That is a high-level design conversation, not this round. - Real money. Payments need idempotency, retries and a reconciliation trail. The moment payments are in scope,
unparkreturning alongis not enough. - Sensors instead of gates. If occupancy comes from hardware events rather than
park()calls, the lot becomes an event consumer and the ticket stops being the source of truth. Different problem; see Pub/Sub & Event-driven.
If you only remember one thing
Get park → Ticket → unpark → bill running end-to-end before you make anything pluggable. A working system that you then refactor for ten minutes always beats a perfectly abstract system that never printed a line.
What it gives you
- Every class maps to something you can point at in a real car park — the design explains itself with no glossary.
- The two rules most likely to change (spot choice, pricing) are interfaces, so the classic follow-ups cost one new class and zero edits.
unpark(id, exitAt)takes time as a parameter, so the entire billing system is unit-testable without waiting or mocking a clock.tryAssignmakes check-and-take atomic on the spot, so multiple gates work without serialising the whole lot.- Small enough to actually finish in 60 minutes, with a
main()that prints real output.
Common mistakes
- Allocation scans every spot — fine for hundreds, wrong for tens of thousands without a per-size free-list.
ParkingFlooris close to a pass-through in a small lot; keeping it can read as ceremony until multi-floor rules actually exist.- Everything is in memory. A restart forgets every open ticket, so real deployments need persistence this design does not have.
- Two strategy interfaces is the right amount of pattern here — but the same instinct, applied to every class, is exactly how this problem gets over-engineered.
- Vehicle-size and spot-size as one ordered scale breaks down the moment a spot has features (EV charger, handicapped, valet-only) as well as a size.
Reference
Code & further reading
A minimal reference implementation and pointers worth bookmarking.
import java.time.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
// ---------- vocabulary ----------
enum VehicleType { BIKE, CAR, TRUCK }
enum SpotSize { SMALL, MEDIUM, LARGE } // ordinal order == physical order
enum TicketStatus { ACTIVE, PAID, LOST }
// ---------- vehicles ----------
abstract class Vehicle {
private final String plate;
private final VehicleType type;
protected Vehicle(String plate, VehicleType type) { this.plate = plate; this.type = type; }
public String plate() { return plate; }
public VehicleType type() { return type; }
public abstract SpotSize requiredSize();
}
class Bike extends Vehicle { public Bike(String p) { super(p, VehicleType.BIKE); } public SpotSize requiredSize() { return SpotSize.SMALL; } }
class Car extends Vehicle { public Car(String p) { super(p, VehicleType.CAR); } public SpotSize requiredSize() { return SpotSize.MEDIUM; } }
class Truck extends Vehicle { public Truck(String p) { super(p, VehicleType.TRUCK); } public SpotSize requiredSize() { return SpotSize.LARGE; } }
// ---------- spot: knows how to be taken safely ----------
class ParkingSpot {
private final String id;
private final int floor;
private final SpotSize size;
private Vehicle vehicle; // null == free
ParkingSpot(String id, int floor, SpotSize size) { this.id = id; this.floor = floor; this.size = size; }
public String id() { return id; }
public int floor() { return floor; }
public SpotSize size() { return size; }
public boolean fits(Vehicle v) { return size.ordinal() >= v.requiredSize().ordinal(); }
public synchronized boolean isFree() { return vehicle == null; }
/** Check and take, indivisibly. Two gates cannot both win. */
public synchronized boolean tryAssign(Vehicle v) {
if (vehicle != null) return false;
vehicle = v;
return true;
}
public synchronized void release() { vehicle = null; }
}
// ---------- ticket: issued once, then only its status moves ----------
class Ticket {
private final String id;
private final Vehicle vehicle;
private final ParkingSpot spot;
private final Instant entryAt;
private TicketStatus status = TicketStatus.ACTIVE;
Ticket(String id, Vehicle v, ParkingSpot s, Instant entryAt) {
this.id = id; this.vehicle = v; this.spot = s; this.entryAt = entryAt;
}
public String id() { return id; }
public Vehicle vehicle() { return vehicle; }
public ParkingSpot spot() { return spot; }
public Instant entryAt() { return entryAt; }
public TicketStatus status(){ return status; }
public void markPaid() { this.status = TicketStatus.PAID; }
}
// ---------- rule 1: which spot? ----------
interface SpotAllocationStrategy {
Optional<ParkingSpot> findSpot(List<ParkingSpot> spots, Vehicle v);
/** Shared by every strategy: free, big enough, and the SMALLEST such size. */
default List<ParkingSpot> candidates(List<ParkingSpot> spots, Vehicle v) {
List<ParkingSpot> fits = spots.stream()
.filter(ParkingSpot::isFree).filter(s -> s.fits(v)).toList();
return fits.stream()
.min(Comparator.comparingInt(s -> s.size().ordinal()))
.map(best -> fits.stream().filter(s -> s.size() == best.size()).toList())
.orElse(List.of());
}
}
class NearestFirst implements SpotAllocationStrategy {
public Optional<ParkingSpot> findSpot(List<ParkingSpot> spots, Vehicle v) {
return candidates(spots, v).stream()
.min(Comparator.comparingInt(ParkingSpot::floor)
.thenComparing(ParkingSpot::id));
}
}
class SpreadOut implements SpotAllocationStrategy {
public Optional<ParkingSpot> findSpot(List<ParkingSpot> spots, Vehicle v) {
Map<Integer, Long> freePerFloor = new HashMap<>();
for (ParkingSpot s : spots)
if (s.isFree()) freePerFloor.merge(s.floor(), 1L, Long::sum);
return candidates(spots, v).stream()
.max(Comparator.comparingLong((ParkingSpot s) -> freePerFloor.getOrDefault(s.floor(), 0L))
.thenComparing(s -> s.id(), Comparator.reverseOrder()));
}
}
// ---------- rule 2: how much? ----------
interface PricingStrategy {
/** Money in paise/cents. Never a double. */
long fee(Ticket t, Instant exitAt);
default long startedHours(Ticket t, Instant exitAt) {
long minutes = Duration.between(t.entryAt(), exitAt).toMinutes();
return Math.max(1, (long) Math.ceil(minutes / 60.0)); // 61 min == 2 h, and never zero
}
}
class HourlyRate implements PricingStrategy {
private static final Map<VehicleType, Long> RATE =
Map.of(VehicleType.BIKE, 1000L, VehicleType.CAR, 2000L, VehicleType.TRUCK, 3000L);
public long fee(Ticket t, Instant exitAt) {
return startedHours(t, exitAt) * RATE.get(t.vehicle().type());
}
}
class SlabRate implements PricingStrategy {
public long fee(Ticket t, Instant exitAt) {
long h = startedHours(t, exitAt);
return h <= 2 ? 5000L : 5000L + (h - 2) * 1500L;
}
}
class LotFullException extends RuntimeException {
LotFullException(VehicleType t) { super("No free spot for a " + t); }
}
// ---------- the front door ----------
class ParkingLot {
private static final int MAX_RETRIES = 3;
private final List<ParkingSpot> spots;
private final Map<String, Ticket> open = new ConcurrentHashMap<>();
private final AtomicInteger seq = new AtomicInteger();
private SpotAllocationStrategy allocation;
private PricingStrategy pricing;
ParkingLot(List<ParkingSpot> spots, SpotAllocationStrategy a, PricingStrategy p) {
this.spots = List.copyOf(spots); this.allocation = a; this.pricing = p;
}
public void setAllocation(SpotAllocationStrategy a) { this.allocation = a; }
public void setPricing(PricingStrategy p) { this.pricing = p; }
public Ticket park(Vehicle v, Instant now) {
for (int i = 0; i < MAX_RETRIES; i++) {
ParkingSpot spot = allocation.findSpot(spots, v)
.orElseThrow(() -> new LotFullException(v.type()));
if (spot.tryAssign(v)) { // lost the race? loop and pick another
Ticket t = new Ticket("T-" + seq.incrementAndGet(), v, spot, now);
open.put(t.id(), t);
return t;
}
}
throw new LotFullException(v.type());
}
public long unpark(String ticketId, Instant exitAt) {
Ticket t = open.remove(ticketId); // remove == this ticket can never bill twice
if (t == null) throw new IllegalArgumentException("Unknown or already-closed ticket: " + ticketId);
long amount = pricing.fee(t, exitAt);
t.markPaid();
t.spot().release();
return amount;
}
public long availableSpots(SpotSize size) {
return spots.stream().filter(ParkingSpot::isFree).filter(s -> s.size() == size).count();
}
}
// ---------- the demo that has to run ----------
public class Main {
public static void main(String[] args) {
List<ParkingSpot> spots = new ArrayList<>();
SpotSize[][] layout = {
{ SpotSize.SMALL, SpotSize.SMALL, SpotSize.MEDIUM, SpotSize.MEDIUM, SpotSize.LARGE },
{ SpotSize.SMALL, SpotSize.MEDIUM, SpotSize.MEDIUM, SpotSize.LARGE, SpotSize.LARGE }
};
for (int f = 0; f < layout.length; f++)
for (int i = 0; i < layout[f].length; i++)
spots.add(new ParkingSpot("F" + (f + 1) + "-" + (i + 1), f, layout[f][i]));
ParkingLot lot = new ParkingLot(spots, new NearestFirst(), new HourlyRate());
Instant nine = Instant.parse("2026-01-01T09:00:00Z");
Ticket bike = lot.park(new Bike("KA-01-0001"), nine);
Ticket car = lot.park(new Car("KA-05-1007"), nine);
Ticket truck = lot.park(new Truck("KA-09-4242"), nine);
System.out.println("parked -> " + bike.spot().id() + ", " + car.spot().id() + ", " + truck.spot().id());
System.out.println("free MEDIUM spots: " + lot.availableSpots(SpotSize.MEDIUM));
System.out.println("car bill = " + lot.unpark(car.id(), nine.plus(Duration.ofHours(3))) + " paise");
lot.setPricing(new SlabRate()); // <-- the follow-up, one line
System.out.println("bike bill = " + lot.unpark(bike.id(), nine.plus(Duration.ofHours(3))) + " paise");
try {
for (int i = 0; i < 5; i++) lot.park(new Truck("KA-09-90" + i), nine);
} catch (LotFullException e) {
System.out.println("rejected -> " + e.getMessage());
}
}
}References & further reading
8 sources- Docsrefactoring.guru
Strategy pattern — Refactoring Guru
The pattern behind both swappable rules in this design, with diagrams in a dozen languages.
- Articlegithub.com
awesome-low-level-design — Parking Lot
A second, independent take on the same problem — worth diffing against your own model.
- Articlegithub.com
Low Level Design Primer
A broad catalogue of machine-coding problems and solutions; good for the next problems after this one.
- Talkyoutube.com
Design a Parking Lot — asked at Google and Facebook
Watch how the clarifying questions are asked out loud. The talking is most of the score.
- Bookdesigngurus.io
Grokking the Object Oriented Design Interview
The course that made this problem the canonical warm-up; parking lot is its opening case study.
- Articlemartinfowler.com
Anemic Domain Model — Martin Fowler
Why a ParkingSpot full of getters and setters, with all the logic in ParkingLot, is the anti-pattern to avoid.
- Articlemartinfowler.com
Value Object — Martin Fowler
The case for a small Money type instead of a double — and for an immutable Ticket.
- Docsdocs.oracle.com
ReentrantLock — Java API docs
If you want per-spot locking with a timeout instead of the synchronized block used here.
Knowledge check
Did it land?
Quick questions, answers revealed on submit. Sign in to save your best score.
question 01 / 08
A bike arrives. Three SMALL spots, two MEDIUM and one LARGE are free. Which spot should the lot hand out?
question 02 / 08
Why does unpark(ticketId, exitAt) take the exit time as a parameter instead of calling the clock inside?
question 03 / 08
The interviewer says: “Now charge a different rate on weekends.” What should your answer be?
question 04 / 08
Two entry gates process arrivals at the same instant and both read that spot F1-03 is free. What is the correct fix?
question 05 / 08
What does the Ticket need to carry for billing to work at all?
question 06 / 08
You are at minute 45 with park() working, unpark() half-written, and no strategy interfaces yet. What do you do?
question 07 / 08
Why model SpotSize as an ordered enum (SMALL < MEDIUM < LARGE) rather than as free-form strings?
question 08 / 08
Which of these is the clearest sign that a parking-lot solution is over-engineered?
0/8 answered