If you remember five things
The whole answer in one glance. Everything below is the defense of these five calls.
- Thin coordinator, fat objects. ParkingLot owns floors and delegates the two interesting decisions out. A spot owns its own fit rule and its own lock. No god class.
- Strategy twice. FeeStrategy and SpotAssignmentStrategy are the two axes of change. Pull them out and the stable core stops getting rewritten when finance reprices or someone adds EV spots.
- Check and claim are one atomic step. The single most important sentence in the room. `tryAssign` does both inside one synchronized block, so two gates scanning the same free spot cannot both win.
- Size by enum ordinal, not subclass explosion. Spots carry a size enum, vehicles carry a size enum, "fits" is an ordinal compare. No SmallSpot/MediumSpot classes that differ by one field.
- Know when NOT to reach for a pattern. No State on the spot, no forced Singleton on the lot. Name the trigger that would change your mind, and you have shown you know where the line is.
1Clarify the requirements#
Stop and scope it before drawing anything. A parking lot can mean a flat lot at a grocery store or a ten-floor airport garage with license-plate cameras.
- Those are different problems with different data structures and different failure modes.
- Spend the first 3 to 5 minutes here, not writing code.
- The answers you get here are load-bearing, so treat them that way.
Here is how I would open:
Candidate: Before I draw anything: is this a flat lot or a multi-floor garage, and do we have different vehicle sizes with spot sizes matched to them?
Interviewer: Multi-floor. Three vehicle sizes, three spot sizes, and a bigger spot can hold a smaller vehicle.
Candidate: Got it. Do we charge money, and if so, flat, hourly, or tiered? And could that pricing change later?
Interviewer: Hourly, and yes, assume finance will reprice it. Design so that does not force a rewrite.
Candidate: Last one, and it is the one that shapes everything: is this a single process, or do many entry gates hit the system at the same time?
Interviewer: Many gates, concurrent. That is where I want you to spend time.
What those answers just told me:
- The interview is about concurrency on spot allocation and pluggable policy, not CRUD.
- "Pricing will change" means pull fee calculation behind an interface so the lot never sees a rate table.
- "Many gates, concurrent" is the interviewer handing me the thesis: the spot claim has to be atomic or two drivers fight over 4B.
Functional requirements
- Three vehicle sizes: motorcycle, car, van.
- Three spot sizes: small, medium, large. A vehicle fits a spot of its own size or bigger.
- Vehicle enters, gets a ticket, parks in a fitting free spot. Vehicle leaves, we compute the fee, free the spot.
- Multiple floors, each with its own set of spots.
- Pricing is hourly and can change later without a rewrite.
Non-functional requirements
- Thread safe. Two gates must never hand out the same spot. This is the single most common correctness bug in this problem.
- Spot lookup should be fast, not a linear scan of every spot in a 5000-spot building.
- Pluggable pricing and spot selection, because interviewers always push on this and I want the seams already cut.
I am scoring this opening before you write a class. I want to hear "is this concurrent?" come from you, not from me. Jump straight into an inheritance tree and I have quietly downgraded you. One opinion worth stating up front: do not model payments as a full gateway here. It eats 10 minutes and earns zero points. Keep a thin Payment abstraction and move on.
2Identify the core entities#
Name the nouns in the requirements and keep only the ones that earn their place. Every class you add is a class the interviewer can ask you to justify, so be stingy.
- Vehicle (abstract) with subtypes Motorcycle, Car, Van. Each carries a size.
- ParkingSpot with a size, a flag for occupied, and the vehicle currently in it.
- ParkingFloor owns a collection of spots and knows how to find a free one.
- ParkingLot is the top level. It owns floors and is the entry point for park and unpark.
- Ticket records the vehicle, the spot, and the entry time. This is the object the gate prints.
- FeeStrategy computes cost from a ticket and exit time. Pluggable.
- SpotAssignmentStrategy picks which free spot to give out. Pluggable.
- EntrancePanel / ExitPanel are the operations surface. I fold these into methods on ParkingLot unless the interviewer wants hardware modeled.
Why split the two strategies out instead of baking them into ParkingLot:
- Those are the two things that change. Everything else is stable.
- Pricing changes every quarter when finance reprices vans.
- Spot selection changes the day someone adds "reserve EV spots near the door."
- Isolate the volatile parts and the stable core stops getting rewritten.
The entity list is table stakes. What earns points is naming the two axes of change (pricing, spot selection) and putting a seam there before the interviewer asks. That one move is the difference between a design that absorbs new requirements and one you rewrite every time the spec shifts.
3Class design#
The shape I aim for: ParkingLot is a thin coordinator.
- It holds floors by composition: floors do not exist without the lot.
- It delegates the two interesting decisions to strategy objects.
- A floor holds spots. A spot holds at most one vehicle. A ticket is a plain record glued to a spot and a vehicle.
Vehicles use inheritance; spots do not:
- Vehicles get subclasses because polymorphism on type is clean and the only real variation is size.
- Spots get a size enum, not subclasses. That saves a class explosion of
SmallSpot,MediumSpot,LargeSpotthat would be identical except for one field.
*-- is composition (the part dies with the whole), o-- is aggregation (the part outlives the whole).Read the relationship arrows out loud, because interviewers read them:
- ParkingLot
*--ParkingFloor is composition: kill the lot and the floors go with it. - ParkingSpot
o--Vehicle is aggregation: the vehicle outlives the spot it sat in. - Getting that distinction right out loud is a cheap signal that you mean what the diagram says.
Here is the park flow at runtime, which is where the concurrency story lives.
Reveal the answer
4Key implementation#
Start with the size model and the vehicle hierarchy. I use enums for sizes so fit logic is an ordinal compare, no lookup table.
- MOTORCYCLE is ordinal 0, VAN is ordinal 2.
- "Fits" means the spot's rank is at least the vehicle's rank.
public enum VehicleSize { MOTORCYCLE, CAR, VAN }
public enum SpotSize { SMALL, MEDIUM, LARGE }
public abstract class Vehicle {
private final String licensePlate;
private final VehicleSize size;
protected Vehicle(String licensePlate, VehicleSize size) {
this.licensePlate = licensePlate;
this.size = size;
}
public VehicleSize getSize() { return size; }
public String getLicensePlate() { return licensePlate; }
}
public class Motorcycle extends Vehicle {
public Motorcycle(String plate) { super(plate, VehicleSize.MOTORCYCLE); }
}
public class Car extends Vehicle {
public Car(String plate) { super(plate, VehicleSize.CAR); }
}
public class Van extends Vehicle {
public Van(String plate) { super(plate, VehicleSize.VAN); }
}
The spot owns the fit rule and its own locking. I keep the lock on the spot, not the lot.
- Two threads fighting over one spot resolve locally.
- A building-wide lock would serialize all 5000 gates behind one mutex. That kills throughput.
public class ParkingSpot {
private final String id;
private final SpotSize size;
private boolean occupied;
private Vehicle vehicle;
public ParkingSpot(String id, SpotSize size) {
this.id = id;
this.size = size;
}
public boolean canFit(Vehicle v) {
return spotRank(size) >= vehicleRank(v.getSize());
}
// atomic claim: returns false if someone beat us to it
public synchronized boolean tryAssign(Vehicle v) {
if (occupied || !canFit(v)) return false;
this.vehicle = v;
this.occupied = true;
return true;
}
public synchronized void release() {
this.vehicle = null;
this.occupied = false;
}
public synchronized boolean isFree() { return !occupied; }
public String getId() { return id; }
private static int spotRank(SpotSize s) { return s.ordinal(); }
private static int vehicleRank(VehicleSize s) { return s.ordinal(); }
}
Notice tryAssign. The check and the claim happen inside one synchronized block. That is the whole trick for correctness.
- Check "is it free" in the strategy and assign later in a separate call, and you have opened a time-of-check-to-time-of-use race.
- Under two gates it will double-book.
- Say the words "check and claim are one atomic step" at the whiteboard. That single sentence is worth real points.
The assignment strategy picks a candidate and tries to claim it, retrying if it lost the race. The loop keeps scanning instead of bailing, so a lost race just moves on to the next free spot.
public interface SpotAssignmentStrategy {
ParkingSpot assign(List<ParkingFloor> floors, Vehicle vehicle);
}
public class NearestFirstStrategy implements SpotAssignmentStrategy {
@Override
public ParkingSpot assign(List<ParkingFloor> floors, Vehicle vehicle) {
for (ParkingFloor floor : floors) {
for (ParkingSpot spot : floor.getSpots()) {
if (spot.canFit(vehicle) && spot.tryAssign(vehicle)) {
return spot; // claimed atomically
}
}
}
return null; // lot full for this size
}
}
Fee calculation lives behind an interface so the interviewer can swap flat, hourly, or weekday/weekend pricing without touching the lot. The hourly rates are concrete on purpose: 10 per hour for a motorcycle, 20 for a car, 40 for a van.
public interface FeeStrategy {
double calculate(Ticket ticket, Instant exitTime);
}
public class HourlyFeeStrategy implements FeeStrategy {
private final Map<VehicleSize, Double> ratePerHour = Map.of(
VehicleSize.MOTORCYCLE, 10.0,
VehicleSize.CAR, 20.0,
VehicleSize.VAN, 40.0
);
@Override
public double calculate(Ticket ticket, Instant exitTime) {
long minutes = Duration.between(ticket.getEntryTime(), exitTime).toMinutes();
long hours = (long) Math.ceil(minutes / 60.0); // round up, charge a full hour
double rate = ratePerHour.get(ticket.getVehicle().getSize());
return Math.max(1, hours) * rate; // minimum one hour
}
}
Finally the lot ties it together. It holds the strategies and keeps a map from ticket id to ticket so exit is O(1), not a search through every parked vehicle.
public class ParkingLot {
private final List<ParkingFloor> floors;
private final SpotAssignmentStrategy assignmentStrategy;
private final FeeStrategy feeStrategy;
private final Map<String, Ticket> activeTickets = new ConcurrentHashMap<>();
public ParkingLot(List<ParkingFloor> floors,
SpotAssignmentStrategy assignmentStrategy,
FeeStrategy feeStrategy) {
this.floors = floors;
this.assignmentStrategy = assignmentStrategy;
this.feeStrategy = feeStrategy;
}
public Ticket parkVehicle(Vehicle vehicle) {
ParkingSpot spot = assignmentStrategy.assign(floors, vehicle);
if (spot == null) {
throw new IllegalStateException("No spot available for " + vehicle.getSize());
}
Ticket ticket = new Ticket(UUID.randomUUID().toString(), Instant.now(), spot, vehicle);
activeTickets.put(ticket.getId(), ticket);
return ticket;
}
public double unparkVehicle(String ticketId) {
Ticket ticket = activeTickets.remove(ticketId);
if (ticket == null) {
throw new IllegalArgumentException("Invalid or already used ticket");
}
double fee = feeStrategy.calculate(ticket, Instant.now());
ticket.getSpot().release();
return fee;
}
}
The constructor takes both strategies as arguments, which is dependency injection doing real work:
- The lot never news up a pricing or assignment policy.
- In a test I hand it a fake strategy and assert the flow in isolation.
- That is the testability win from keeping the lot thin.
I leave floors and tickets as plain holders. They are glue, not logic. Padding the design with getters would burn whiteboard time I would rather spend on concurrency, and the shape is already readable from the classes above.
5Design decisions and patterns used#
Strategy, used twice, and that is deliberate.
FeeStrategyandSpotAssignmentStrategyare the two axes of change.- Pull them out and ParkingLot stops caring how we price or where we park.
- Swap
NearestFirstStrategyfor aRandomStrategyorKeepFloorsBalancedStrategywithout touching the lot. - Swap
HourlyFeeStrategyfor a weekend rate the same way.
Factory to build vehicles from a gate reading.
- A
VehicleFactory.create(type, plate)keeps the "which subclass" decision in one place. - The rest of the code deals in
Vehicleand never writes aswitchon type scattered across files.
Singleton is tempting for ParkingLot, and I resist the hard version.
- There is usually one building, so I mention it.
- A hard singleton makes the thing miserable to test: you cannot stand up two isolated lots in two test cases.
- I prefer one instance wired at startup via dependency injection. That gives you "one in production" without poisoning the tests.
Observer is my follow-up answer, not my first draft.
- When the interviewer asks "how does the entrance sign show FULL," an observer on spot count that notifies a display board is the clean move.
- I do not build it unprompted, because speculative patterns are a red flag.
State, intentionally not used on the spot.
- A boolean
occupiedflag plus a vehicle reference covers every case here. - State pattern would be over-engineering.
- I reach for it only if spots gain a real lifecycle (reserved, out of service, charging) with transition rules, and I say that trigger out loud so the interviewer sees I know where the line is.
Bolting on three patterns you do not need reads as pattern-matching, not judgment. Saying "State is overkill here, I would add it only when spots get a real lifecycle" shows you know what each pattern costs and when it pays. Knowing when NOT to use a pattern is a clearer senior signal than knowing the pattern.
6Handling edge cases and concurrency#
This is the part that separates a passing answer from a strong one, and it is where I spend the back half of the interview.
The double-booking race.
- Two cars arrive at two gates at the same instant. Both strategies scan and both see spot 4B free.
- Without protection, both get assigned and one driver finds a car already in "their" spot.
tryAssigncloses this: the check and the claim are one synchronized unit, so exactly one thread wins and the loser keeps scanning.- I lock per spot, not the whole lot, so throughput stays high even with many gates hammering the system.
Full lot.
- The strategy returns null and
parkVehiclethrows a clear exception the gate turns into "no space for vans." - It can be full for vans but open for motorcycles at the same moment, so the message is size-specific, not a blanket "lot full."
Double exit or reused ticket.
activeTickets.removeis atomic in aConcurrentHashMap.- The first exit gets the ticket, the second gets null and we reject it with "Invalid or already used ticket."
- That same remove-returns-null guard stops a released spot from being released twice, which would otherwise let a ghost vehicle linger.
Clock and rounding.
- I round the fee up to the full hour and enforce a one-hour minimum, so 1 minute and 59 minutes both bill as one hour.
- Decide this out loud, because a pedantic interviewer will ask what happens at 61 minutes.
- Answer: it rounds up to 2 hours, so a car pays 40.
A vehicle in a bigger spot.
- A motorcycle can take a large spot when nothing else is free.
canFitallows it, and it is correct behavior. - Note that it wastes a large spot a van might have wanted.
- If they care, add a strategy that prefers the tightest fit by scanning the smallest eligible size first.
Scaling the lookup.
- Scanning every spot is fine for a demo but ugly for a 5000-spot garage, where every park is O(n).
- In the room I would say: keep a per-floor queue of free spots per size, so assignment is a poll off a queue instead of a full scan.
- I keep the scan in code for clarity and call out the upgrade, because showing I know the honest version is worth more than silently shipping the slow one.
Hand-waving "we would add a lock somewhere" without pointing at the exact two lines that race. Pointing at tryAssign and naming the time-of-check-to-time-of-use window is the moment I decide you have cleared the concurrency bar. Candidates who stay vague about where the lock goes are the ones who have not actually written concurrent code.
7Extensions and follow-ups#
Where interviewers usually push, and how I answer:
- Payments. Add a
Paymentinterface withCashandCardimplementations. Keep it behindunparkVehicleso the lot only knows "collect this amount" and never learns about card networks. - Reserved and EV spots. A new spot attribute plus a strategy that honors reservations. The strategy seam already supports this, so it is a new class, not a rewrite.
- License plate entry and lost ticket. Index
activeTicketsby plate as a secondary map, so a driver who lost the ticket can still exit (for a penalty fee) by scanning the plate. - Real-time availability display. Observer pattern: a
DisplayBoardsubscribes to spot-count changes per floor and per size. - Distributed, many buildings. Now the per-spot lock is not enough, because
synchronizedonly guards one JVM. Move spot state to a store with atomic compare-and-set, andtryAssignbecomes a conditional DB write (claim only if still free). The class design barely changes, which is the whole point of isolating the claim in one method.
"Two gates hit the same free spot at the same instant. How do you stop a double-book?"
"There are multiple entrances and the lot is huge. How does a gate find a spot without scanning 5000 spots every time?"
"Finance wants weekend pricing and a flat daily cap. How much of the code changes?"
"A motorcycle just took your last large spot and now a van cannot park. Is that a bug?"
"Now make it a chain: many buildings, shared availability. What breaks?"
What you can now explain in the room
- Why you scope the lot (floors, sizes, pricing, concurrency) before writing a single class.
- Why Strategy appears twice, and which two axes of change it isolates.
- Why check-and-claim must be one atomic step, and exactly which two lines race without it.
- Why you lock per spot instead of per lot, and what that buys for throughput.
- Why spots use a size enum while vehicles use subclasses.
- When to skip Singleton and State, and the exact trigger that would change your mind.
Next, try these LLD problems that reuse the same muscles: Design an Elevator System (state and scheduling strategy), Design a Vending Machine (State pattern done right), and Design a Movie Ticket Booking System (concurrency on seat selection).
Get a prep plan built around your Design a Parking Lot interview
Tell us the role and your interview date. We will send back a dated, day by day plan. Free.