- Spend the first 5 minutes pinning down scope: vehicle types, spot sizes, floors, gates, pricing rules and payment. Write the agreed list down.
- The model is small:
ParkingLot(facade) →ParkingFloor→ParkingSpot, plusVehicle,Ticket,Receipt. - Two Strategy seams carry the design:
SpotAllocationStrategy(which spot) andPricingStrategy(how much). A decorator adds a daily cap; an observer drives display boards. - The one real concurrency bug is two gates handing out the same spot. Fix it by making "claim this spot" atomic, not by locking the whole lot.
- Full runnable Java 17 code below, with a dry run, extensions (EV, reservations, passes, ANPR) and the mistakes that cost candidates the round.
The parking lot question is the "Two Sum" of low level design. Almost every Indian product company and many startups have used it in an LLD or machine coding round, which means interviewers have seen hundreds of answers and know exactly where weak ones break. They are not checking whether you know that a parking lot has floors. They are checking whether you can turn a vague prompt into a small, extensible, correct object model, and whether your code survives the follow-up questions.
This guide is written the way you should answer: requirements first, then entities, then the class diagram, then the patterns that earn their place, then complete code. If you want the broader context on how LLD rounds are run, read the LLD interview preparation guide first; if your round is a timed coding exercise, pair this with the machine coding round guide.
Step 1: Clarify Requirements for the Parking Lot
The prompt is usually one line: "Design a parking lot." The interviewer is waiting to see whether you start coding or start asking. Ask, but ask efficiently: propose sensible defaults and let the interviewer correct you, rather than firing twenty open questions.
Questions worth asking (with the defaults I would propose)
| Question | Proposed default | Why it matters for the design |
|---|---|---|
| Which vehicle types? | Bike, car, truck | Drives the VehicleType enum and compatibility rules |
| Which spot sizes, and can a small vehicle use a bigger spot? | Small, medium, large; a bike may use any, a car medium or large, a truck only large | Allocation must try preferred size first, then fall back |
| Multiple floors? Multiple entry/exit gates? | Yes and yes | Multiple gates means concurrent allocation, which is the main correctness risk |
| How is the fee computed? | Flat first hour + per started hour after, by vehicle type, daily cap | Pricing becomes a strategy, the cap a decorator |
| Payment modes? | Cash, UPI, card at exit | A PaymentProcessor interface; no gateway internals |
| Display boards showing free spots? | Yes, per floor and per size | Observer of park/unpark events |
| Reservations, EV charging, monthly passes? | Out of scope now, design should allow them | Tells you which seams to leave open |
Agreed functional requirements
- A vehicle arrives at any entry gate; the system assigns a compatible free spot and issues a ticket. If none is free, it rejects the vehicle.
- At exit, the system takes the ticket, computes the fee, collects payment and frees the spot.
- The same vehicle cannot be parked twice at the same time, and a ticket cannot be used to exit twice.
- Display boards show free spots per size, updated on every park and exit.
- Pricing and allocation rules can change without rewriting the core.
Non-functional requirements
This is an in-memory, single-process design (that is what LLD means here). The non-functional points that matter are thread safety for concurrent gates, extensibility for new vehicle types and pricing, and testability, which is why times are passed in rather than read from the system clock inside business logic. If the interviewer pushes toward "a parking app for the whole city with millions of users", that is a high level design question; the HLD vs LLD guide explains how to steer between the two.
Step 2: Identify the Core Entities
A reliable trick is to underline the nouns in your requirements and then delete the ones that carry no behaviour or state of their own. "Gate" survives only as a caller; "display board" survives as a listener; "parking attendant" disappears entirely.
| Entity | Responsibility | Key state |
|---|---|---|
ParkingLot | Facade: park, unpark, availability; owns tickets | floors, active tickets, parked vehicle numbers, strategies |
ParkingFloor | Groups spots; answers free counts | floor number, spots by size |
ParkingSpot | Atomically claimed and released | id, floor, size, current vehicle |
Vehicle | Immutable value: number + type | registration number, type |
Ticket | Proof of entry | id, vehicle, spot, entry time |
Receipt | Proof of exit and payment | ticket id, hours billed, amount |
SpotAllocationStrategy | Find and claim a spot | stateless |
PricingStrategy | Compute fee | rate tables |
PaymentProcessor | Collect money | none in this design |
ParkingEventListener | React to park/exit (display boards, analytics) | none |
abstract class Vehicle with Car, Bike, Truck subclasses. They have no different behaviour, only a different type, so an enum is cleaner. Subclasses are justified only when behaviour differs, for example an ElectricCar that needs a charger. Saying this out loud shows judgment.Step 3: Parking Lot Class Diagram (UML in Text)
Draw this on the whiteboard or in a comment block before writing code. Arrows show "has" (◆) and "implements" (△) relationships.
Notice what is not there: no Admin, no Database, no Attendant. Interviewers reward a model that is as small as the requirements allow and no smaller.
Step 4: Design Patterns That Earn Their Place
Strategy pattern for spot allocation
"Which spot should this car get?" has several reasonable answers: the lowest floor (shortest walk), the floor with the most free spots (spreads traffic), the spot nearest the exit, or a reserved block for VIP passes. These are interchangeable algorithms with the same signature, which is the textbook case for Strategy. ParkingLot depends on the interface; the concrete strategy is injected at construction.
Strategy pattern for pricing
Pricing changes more often than any other rule in a real lot: weekend rates, event-day surge at a stadium, mall validation discounts, monthly passes. Putting it behind PricingStrategy means every one of those is a new class, which is the Open/Closed principle in practice. The SOLID principles guide uses exactly this kind of example.
Decorator for the daily cap
A daily cap ("never more than ₹300 per 24 hours for a car") applies on top of whatever base pricing you use. Instead of copying the cap logic into every strategy, wrap: new DailyCapPricing(new HourlyPricing(), caps). The decorator implements the same interface and delegates, then clamps.
Observer for display boards
Display boards, an analytics counter and an "almost full" SMS alert to the operator all want to know when a spot changes. ParkingLot publishes events to a list of ParkingEventListeners and knows nothing about what they do.
Why not Singleton?
Many tutorials make ParkingLot a Singleton. It is not wrong to mention, but a hard-coded getInstance() makes unit tests share state and makes the inevitable "now support 5 lots in Bengaluru" follow-up awkward. Construct one instance at startup and inject it. If the interviewer specifically asks for Singleton, show it, and mention this trade-off. For the full catalogue of patterns and when each is overkill, see the top 10 design patterns for interviews.
Step 5: Complete Java Code for the Parking Lot
This is a single file you can run with java ParkingLotDemo.java on Java 17+. In a real machine coding round you would split it into packages (model, strategy, service); a single file keeps the listing readable here.
Enums, value objects and the spot
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicLong;
public class ParkingLotDemo {
enum VehicleType { BIKE, CAR, TRUCK }
enum SpotSize { SMALL, MEDIUM, LARGE }
/** Allowed spot sizes for a vehicle, in order of preference (smallest fit first). */
static List<SpotSize> allowedSizes(VehicleType type) {
return switch (type) {
case BIKE -> List.of(SpotSize.SMALL, SpotSize.MEDIUM, SpotSize.LARGE);
case CAR -> List.of(SpotSize.MEDIUM, SpotSize.LARGE);
case TRUCK -> List.of(SpotSize.LARGE);
};
}
record Vehicle(String number, VehicleType type) { }
record Ticket(String id, Vehicle vehicle, ParkingSpot spot, LocalDateTime entryTime) { }
record Receipt(String ticketId, String vehicleNumber, long hoursBilled, long amountRupees) { }
static final class ParkingSpot {
private final String id;
private final int floor;
private final SpotSize size;
private Vehicle vehicle; // guarded by "this"
ParkingSpot(String id, int floor, SpotSize size) {
this.id = id;
this.floor = floor;
this.size = size;
}
/** Atomically claims the spot. Returns false if someone else got it first. */
synchronized boolean tryPark(Vehicle v) {
if (vehicle != null) return false;
vehicle = v;
return true;
}
synchronized void release() { vehicle = null; }
synchronized boolean isFree() { return vehicle == null; }
String id() { return id; }
int floor() { return floor; }
SpotSize size() { return size; }
@Override public String toString() { return id; }
}
static final class ParkingFloor {
private final int number;
private final Map<SpotSize, List<ParkingSpot>> spotsBySize = new EnumMap<>(SpotSize.class);
ParkingFloor(int number, int small, int medium, int large) {
this.number = number;
addSpots(SpotSize.SMALL, small);
addSpots(SpotSize.MEDIUM, medium);
addSpots(SpotSize.LARGE, large);
}
private void addSpots(SpotSize size, int count) {
List<ParkingSpot> list = new ArrayList<>();
for (int i = 1; i <= count; i++) {
list.add(new ParkingSpot("F" + number + "-" + size.name().charAt(0) + i, number, size));
}
spotsBySize.put(size, List.copyOf(list));
}
int number() { return number; }
List<ParkingSpot> spots(SpotSize size) { return spotsBySize.get(size); }
long freeCount(SpotSize size) {
return spotsBySize.get(size).stream().filter(ParkingSpot::isFree).count();
}
}
The spot list per floor is immutable (List.copyOf): spots are created once and only their occupancy changes. That removes a whole class of concurrency bugs, because nobody can add or remove spots while an allocator iterates.
Allocation strategies
interface SpotAllocationStrategy {
/** Finds AND claims a compatible spot, or returns empty if the lot is full for this vehicle. */
Optional<ParkingSpot> allocate(List<ParkingFloor> floors, Vehicle vehicle);
}
/** Shortest walk: lowest floor first, smallest compatible size first. */
static final class LowestFloorFirst implements SpotAllocationStrategy {
@Override
public Optional<ParkingSpot> allocate(List<ParkingFloor> floors, Vehicle vehicle) {
for (ParkingFloor floor : floors) {
for (SpotSize size : allowedSizes(vehicle.type())) {
for (ParkingSpot spot : floor.spots(size)) {
if (spot.tryPark(vehicle)) return Optional.of(spot);
}
}
}
return Optional.empty();
}
}
/** Spreads load: try floors in descending order of free spots of the preferred size. */
static final class MostFreeFloorFirst implements SpotAllocationStrategy {
@Override
public Optional<ParkingSpot> allocate(List<ParkingFloor> floors, Vehicle vehicle) {
for (SpotSize size : allowedSizes(vehicle.type())) {
List<ParkingFloor> ordered = new ArrayList<>(floors);
ordered.sort(Comparator.comparingLong((ParkingFloor f) -> f.freeCount(size)).reversed());
for (ParkingFloor floor : ordered) {
for (ParkingSpot spot : floor.spots(size)) {
if (spot.tryPark(vehicle)) return Optional.of(spot);
}
}
}
return Optional.empty();
}
}
The important design decision is that allocate finds and claims in one step through tryPark. If it only "found" a free spot and the caller parked later, two gates could find the same spot in between. Free counts read during sorting may be slightly stale under concurrency, and that is fine: they only affect preference order, never correctness.
Pricing strategy and the daily-cap decorator
interface PricingStrategy {
long fee(Ticket ticket, LocalDateTime exitTime);
}
/** Billable hours: every started hour counts, minimum one hour. */
static long billableHours(LocalDateTime entry, LocalDateTime exit) {
long minutes = Math.max(0, Duration.between(entry, exit).toMinutes());
return Math.max(1, (minutes + 59) / 60);
}
/** Flat first hour + rate for each additional started hour, per vehicle type. */
static final class HourlyPricing implements PricingStrategy {
private final Map<VehicleType, long[]> rates = new EnumMap<>(VehicleType.class);
HourlyPricing() {
rates.put(VehicleType.BIKE, new long[] {20, 10}); // first hour, each extra hour (Rs)
rates.put(VehicleType.CAR, new long[] {50, 30});
rates.put(VehicleType.TRUCK, new long[] {100, 60});
}
@Override
public long fee(Ticket t, LocalDateTime exitTime) {
long hours = billableHours(t.entryTime(), exitTime);
long[] r = rates.get(t.vehicle().type());
return r[0] + (hours - 1) * r[1];
}
}
/** Decorator: never charge more than a cap per started 24-hour block. */
static final class DailyCapPricing implements PricingStrategy {
private final PricingStrategy inner;
private final Map<VehicleType, Long> capPerDay;
DailyCapPricing(PricingStrategy inner, Map<VehicleType, Long> capPerDay) {
this.inner = inner;
this.capPerDay = capPerDay;
}
@Override
public long fee(Ticket t, LocalDateTime exitTime) {
long days = (billableHours(t.entryTime(), exitTime) + 23) / 24;
long cap = capPerDay.get(t.vehicle().type()) * days;
return Math.min(inner.fee(t, exitTime), cap);
}
}
Payment, listeners and the ParkingLot facade
interface PaymentProcessor {
boolean pay(String ticketId, long amountRupees);
}
static final class UpiPayment implements PaymentProcessor {
@Override public boolean pay(String ticketId, long amount) {
System.out.println("UPI collect of Rs " + amount + " for " + ticketId + " succeeded");
return true;
}
}
interface ParkingEventListener {
void onParked(Ticket ticket);
void onExited(Ticket ticket, Receipt receipt);
}
static final class DisplayBoard implements ParkingEventListener {
private final ParkingLot lot;
DisplayBoard(ParkingLot lot) { this.lot = lot; }
@Override public void onParked(Ticket t) { show(); }
@Override public void onExited(Ticket t, Receipt r) { show(); }
private void show() { System.out.println(" [Board] free spots: " + lot.availability()); }
}
static final class ParkingFullException extends RuntimeException {
ParkingFullException(String msg) { super(msg); }
}
static final class ParkingLot {
private final List<ParkingFloor> floors;
private final SpotAllocationStrategy allocation;
private final PricingStrategy pricing;
private final Map<String, Ticket> activeTickets = new ConcurrentHashMap<>();
private final Set<String> parkedVehicles = ConcurrentHashMap.newKeySet();
private final List<ParkingEventListener> listeners = new CopyOnWriteArrayList<>();
private final AtomicLong ticketSeq = new AtomicLong();
ParkingLot(List<ParkingFloor> floors, SpotAllocationStrategy allocation, PricingStrategy pricing) {
this.floors = List.copyOf(floors);
this.allocation = allocation;
this.pricing = pricing;
}
void addListener(ParkingEventListener l) { listeners.add(l); }
Ticket park(Vehicle vehicle, LocalDateTime at) {
if (!parkedVehicles.add(vehicle.number())) {
throw new IllegalStateException(vehicle.number() + " is already parked");
}
Optional<ParkingSpot> spot = allocation.allocate(floors, vehicle);
if (spot.isEmpty()) {
parkedVehicles.remove(vehicle.number());
throw new ParkingFullException("No spot for " + vehicle.type());
}
Ticket ticket = new Ticket("T" + ticketSeq.incrementAndGet(), vehicle, spot.get(), at);
activeTickets.put(ticket.id(), ticket);
listeners.forEach(l -> l.onParked(ticket));
return ticket;
}
Receipt unpark(String ticketId, LocalDateTime at, PaymentProcessor payment) {
Ticket ticket = activeTickets.remove(ticketId); // atomic: a ticket can exit only once
if (ticket == null) throw new IllegalArgumentException("Invalid or used ticket " + ticketId);
long amount = pricing.fee(ticket, at);
if (!payment.pay(ticketId, amount)) {
activeTickets.put(ticketId, ticket); // roll back so the driver can retry
throw new IllegalStateException("Payment failed for " + ticketId);
}
ticket.spot().release();
parkedVehicles.remove(ticket.vehicle().number());
Receipt receipt = new Receipt(ticketId, ticket.vehicle().number(),
billableHours(ticket.entryTime(), at), amount);
listeners.forEach(l -> l.onExited(ticket, receipt));
return receipt;
}
Map<SpotSize, Long> availability() {
Map<SpotSize, Long> free = new EnumMap<>(SpotSize.class);
for (SpotSize s : SpotSize.values()) {
free.put(s, floors.stream().mapToLong(f -> f.freeCount(s)).sum());
}
return free;
}
}
public static void main(String[] args) {
List<ParkingFloor> floors = List.of(
new ParkingFloor(0, 1, 1, 1),
new ParkingFloor(1, 2, 2, 1));
PricingStrategy pricing = new DailyCapPricing(new HourlyPricing(),
Map.of(VehicleType.BIKE, 100L, VehicleType.CAR, 300L, VehicleType.TRUCK, 600L));
ParkingLot lot = new ParkingLot(floors, new LowestFloorFirst(), pricing);
lot.addListener(new DisplayBoard(lot));
LocalDateTime t0 = LocalDateTime.of(2026, 10, 10, 9, 0);
Ticket car1 = lot.park(new Vehicle("KA01AB1234", VehicleType.CAR), t0);
Ticket car2 = lot.park(new Vehicle("KA05MN4321", VehicleType.CAR), t0.plusMinutes(2));
Ticket bike = lot.park(new Vehicle("MH12XY9876", VehicleType.BIKE), t0.plusMinutes(5));
System.out.println(car1.spot() + " " + car2.spot() + " " + bike.spot());
System.out.println(lot.unpark(car1.id(), t0.plusMinutes(150), new UpiPayment()));
System.out.println(lot.unpark(car2.id(), t0.plusHours(30), new UpiPayment()));
try {
lot.unpark(car1.id(), t0.plusMinutes(200), new UpiPayment());
} catch (IllegalArgumentException e) {
System.out.println("Rejected: " + e.getMessage());
}
}
}
Step 6: Dry Run of the Demo
Walking the interviewer through a dry run is the fastest way to prove your code works without a compiler. Floor 0 has spots F0-S1, F0-M1, F0-L1; floor 1 has F1-S1, F1-S2, F1-M1, F1-M2, F1-L1.
- Car 1 at 09:00.
LowestFloorFirststarts at floor 0, preferred size MEDIUM,F0-M1.tryParksucceeds. Ticket T1. Board: SMALL 3, MEDIUM 2, LARGE 2. - Car 2 at 09:02. Floor 0 MEDIUM is taken, so it falls back to floor 0 LARGE:
F0-L1. Ticket T2. This is a design choice worth flagging: lowest-floor-first will put a car in a truck spot before going upstairs. If trucks are common, swap the loop order (size outer, floor inner) or useMostFreeFloorFirst. Being able to change that by swapping a class is the whole point of the strategy. - Bike at 09:05.
F0-S1. Ticket T3. - Car 1 exits at 11:30. 150 minutes → 3 billable hours → ₹50 + 2 × ₹30 = ₹110. Cap for 1 day is ₹300, so ₹110 is charged. Spot
F0-M1is released. - Car 2 exits next day at 15:00. 29 h 58 min → 30 billable hours. Hourly would be ₹50 + 29 × ₹30 = ₹920. It spans 2 started days, so cap = ₹600. Charged ₹600.
- Car 1's ticket is presented again.
activeTickets.removereturns null, so the second exit is rejected. No double release, no double charge.
If the interviewer asks to make allocation O(1), keep a free-spot queue per (floor, size): a ConcurrentLinkedQueue<ParkingSpot> you poll() to allocate and offer() to release. Poll is atomic, so it also solves the concurrency problem. The trade-off is that "nearest spot" ordering becomes harder; a PriorityBlockingQueue ordered by distance gives O(log n) with ordering.
Step 7: Concurrency with Multiple Entry Gates
The interviewer's favourite follow-up: "Gate A and Gate B both see spot F1-M1 free at the same instant. What happens?" In a naive design both write a ticket for the same spot and two cars meet at one bay. The fix options, from simplest to most scalable:
| Approach | How | Trade-off |
|---|---|---|
| Lock the whole lot | synchronized park() | Correct but serialises every gate; fine for a small lot, weak answer |
| Atomic claim per spot (used above) | synchronized tryPark returns false if taken | Gates only contend on the same spot; simple and correct |
| Compare-and-set | AtomicReference<Vehicle>.compareAndSet(null, v) | Lock-free; same semantics as above |
| Free-spot queue per floor/size | ConcurrentLinkedQueue.poll() | O(1) and atomic; ordering is FIFO not nearest |
Two more races are handled in the code and worth pointing out. The same vehicle entering through two gates is blocked by parkedVehicles.add, which is atomic on a concurrent set. The same ticket exiting twice is blocked because ConcurrentHashMap.remove returns the ticket to exactly one caller. If your interviewer goes deeper into Java memory model questions, the Java multithreading interview questions post covers synchronized, CAS and concurrent collections in detail.
unpark the ticket is removed before payment, so a second exit attempt cannot race in. If payment fails, it is put back. Candidates who free the spot before payment succeeds create a state where the car is still in the bay but the system thinks it is free.Step 8: Extensions Interviewers Ask (and Where They Plug In)
A good design is measured by how little existing code changes when requirements grow. For each follow-up, say which class you add and which you leave untouched.
EV charging spots
Add SpotSize-independent features: give ParkingSpot a Set<SpotFeature> (CHARGER, COVERED, ACCESSIBLE), and give Vehicle required features. The allocation strategy filters on spot.features().containsAll(vehicle.needs()). Charging cost becomes a second pricing component: a CompositePricing that sums parking fee and kWh fee.
Reservations
A spot gains a third state, RESERVED, with a reservation id and expiry. tryPark accepts a reserved spot only for the vehicle holding that reservation. A scheduled sweeper releases expired reservations. Now spot state has three values and legal transitions, which is a natural place to mention the State pattern, though an enum plus guarded transitions is usually enough.
Monthly passes and FASTag/ANPR entry
Monthly passes are a PricingStrategy that returns 0 when the vehicle number has an active pass and delegates otherwise, which is another decorator. Automatic number plate recognition or FASTag at the gate changes only how the Vehicle is identified and how PaymentProcessor collects; ParkingLot does not change.
Lost ticket
Because parkedVehicles is keyed by registration number, add an index from vehicle number to ticket, look the ticket up by number, and apply a LostTicketPricing decorator that charges the maximum daily rate as a penalty.
Multiple lots in a city
This is why we avoided Singleton. A ParkingLotRegistry maps lot id to ParkingLot. Beyond that, persistence, a booking app and city-wide search are high level design topics; the patterns for reservations with contention are covered in BookMyShow seat booking design.
The 45-Minute Parking Lot Interview Playbook
Requirements
Propose defaults, confirm vehicle types, spot sizes, gates, pricing, payment. Write them down and mark what is out of scope.
Entities + diagram
List entities with one-line responsibilities. Sketch the diagram. Name the two strategies explicitly.
Code the core
Enums, Spot with atomic claim, Floor, one allocation strategy, one pricing strategy, the facade. Skip getters you do not need.
Dry run + extensions
Run one park/exit cycle aloud, then handle concurrency and one or two extensions by naming the class you would add.
What interviewers are actually scoring
- Scoping: did you ask the questions that change the design (spot compatibility, multiple gates) and skip the ones that don't (colour of the ticket)?
- Responsibility placement: fee logic in pricing, not in
TicketorVehicle; claim logic on the spot, not scattered. - Extensibility you can demonstrate: "weekend pricing is a new class; nothing else changes" is better than drawing ten interfaces up front.
- Correctness under concurrency: at mid and senior levels this is often the deciding signal.
- Code quality: immutability for value objects, clear names, no god class, no stringly-typed vehicle types.
If you can explain your design decisions clearly while coding, you are already ahead of most candidates; the think-out-loud communication guide applies to LLD as much as DSA.
Common Mistakes in Parking Lot LLD
- Starting with classes before requirements. You end up modelling an attendant, a database and a CCTV camera that nobody asked for.
- Vehicle subclasses with no behaviour.
Car extends Vehiclethat only overridesgetType()is an enum in disguise. - Pricing inside ParkingLot with an if-else ladder. Every new rule edits the core class, which is exactly what the interviewer will ask you to change next.
- Find-then-park. Returning a free spot and claiming it later is a race. Claim atomically.
- Freeing the spot before payment. Leaves the system inconsistent on payment failure.
- Reading
LocalDateTime.now()inside pricing. Untestable; pass the time in or inject aClock. - Over-engineering. Abstract factories for spots, a builder for tickets and five layers of interfaces in a 45-minute round signal that you cannot prioritise. Add patterns when a requirement demands them.
- Floating-point money. Use
longin rupees or paise, orBigDecimal; neverdouble.
Once parking lot feels comfortable, the same skills transfer directly: the elevator system adds scheduling and state, and the vending machine is the cleanest way to practise the State pattern.
Frequently Asked Questions
Crack LLD and Machine Coding Rounds
Prepflix's interview course walks through parking lot, elevator, Splitwise and a dozen more LLD problems with live code reviews so you learn the judgment, not just the answer.
Explore the Prepflix Course →