Design a Parking Lot: A Level-by-Level LLD Interview Walkthrough
Loading…
Loading…
"Design a parking lot" is the first question in most LLD rounds, and the majority of candidates treat it as a class enumeration exercise. They list ParkingLot, Floor, Slot, Vehicle, Ticket, and Payment, draw arrows between them, and consider it done. The interviewer sits through this without interrupting because the score is not in the list.
The real question being probed is whether you can identify the three independent axes of change this system actually experiences and design each axis to be independently extensible: vehicle types expand (motorcycles, EVs, oversized trucks), pricing rules change (flat rate, hourly, dynamic surge), and payment methods rotate (cash, card, contactless, monthly pass). A design that hardcodes fee logic in the Ticket class or scatters slot type checks across if-else chains will require structural rewrites when any one of those axes moves. The defensible take this walkthrough earns: the Parking Lot problem is not about inheritance depth. It is about isolating the three axes of change so that adding a new vehicle type, a new fee structure, or a new payment method each requires touching exactly one class.
The prompt is intentionally bare. Scoping it is the first scored moment.
Functional:
Non-functional, and this is where the design decisions actually live:
"I'd scope this as: vehicle enters, system finds the right slot type, issues a ticket with entry time and slot assignment. Vehicle exits, system retrieves the ticket, calculates the fee based on time parked, processes payment, and marks the slot free. I'd model Compact, Large, Handicapped, and Electric slot types and Car, Motorcycle, Truck, and Electric vehicle types."
"What non-functional concern would change your class design?"
A Mid answer that lists the classes without mentioning concurrent access or that fee rules change independently reveals they scoped only the happy path. That is the boundary.
"Two non-functional requirements will change the design. First, concurrent entries: two vehicles arriving at the same time must not be assigned the same slot, which means slot assignment has to be an atomic check and claim operation, not a read followed by a separate write. Second, fee rules change on a business cycle, so fee calculation has to be behind an interface that can be swapped without touching the Ticket or Payment classes."
"How does the concurrency requirement change which layer you put the slot assignment logic in?"
Senior is expected to name both concurrency and fee changeability as design drivers, not just functional requirements. Mid names the classes. Senior names the forces that shape them.
"Before I draw anything, I want to name the three independent dimensions this system changes along: vehicle types expand, fee structures change on a business schedule, and payment integrations rotate out every few years. If those three axes share code paths, fee logic in the Ticket class or slot type checks in ParkingLot, every change touches a wide surface. I'd scope the design as three extension points: a SlotMatcher interface for vehicle to slot assignment, a FeeCalculator interface behind which any pricing rule plugs in, and a PaymentProcessor interface. Everything else is plumbing."
"You named three extension points before drawing a diagram. What do you say if the interviewer pushes back and calls it over-engineering?"
Staff is expected to identify axes of change before committing to a structure, and to defend the extension points with a concrete scenario: adding EV charging pricing with a FeeCalculator interface is one new class. Without it, the fee method grows a branch and gains a reason to break. That is not over-engineering, it is isolating a known change.
The core object model has six primary classes. The important thing is not naming them correctly. It is assigning responsibilities so that each class has exactly one reason to change.
The responsibility split that matters:
canFit(vehicle) decision polymorphically. LargeSlot says it can fit Cars and Trucks. CompactSlot says it can fit Motorcycles and Cars. ElectricSlot says it fits only Electric vehicles. No ParkingLot if-else chain and no vehicle type switch."I'd have ParkingLot at the top, which owns multiple ParkingFloors, each of which has a list of ParkingSlots. A ParkingSlot holds a reference to the Vehicle parked there when occupied. When a vehicle enters, ParkingLot finds a matching slot, stamps a Ticket with the entry time and slot id, and returns it. On exit, it retrieves the ticket, computes the fee, and creates a Receipt."
"Where does the fee calculation logic live, specifically which class and which method?"
A Mid who puts fee calculation inside the Ticket class or inside ParkingLot.exit() as a direct computation has given the Ticket class a second reason to change. That is the boundary and the interviewer will probe it immediately.
"I'd pull fee calculation out of Ticket entirely and put it behind a FeeCalculator interface, because fee rules change on a business cycle and Ticket is about data, not computation. ParkingLot holds a reference to the active FeeCalculator and calls it on exit. For slot matching I'd put a canFit(vehicle) method on ParkingSlot rather than a type switch in ParkingFloor, so adding a new slot type is adding a new class, not editing a switch."
"canFit is on the slot, not the vehicle. Why that direction? What if you reversed it?"
Senior is expected to separate fee from Ticket using a named pattern (Strategy) and explain the canFit direction: the slot knows its own physical constraints, so the slot owns canFit. Reversing it (vehicle.canFitIn(slot)) leaks slot knowledge into Vehicle and couples Vehicle to every slot variant.
"The core question is: what changes independently? Fee rules change without touching vehicles or slots. Slot constraints are a property of the physical hardware, not the software flow. Vehicle types expand based on product decisions. If these three things share methods or classes they become coupled changes. I'd define three interfaces, SlotMatcher, FeeCalculator, and PaymentProcessor, and have ParkingLot depend on abstractions rather than on any specific implementation. This also makes the system testable: I can inject a FlatRateFeeCalculator in tests without a real pricing engine, and I can simulate a full lot by injecting a no available slots SlotMatcher."
"You have three interfaces in ParkingLot's constructor. How does that interact with a Singleton pattern for ParkingLot?"
Staff is expected to arrive at dependency injection as a consequence of interface segregation, and to recognize that a Singleton ParkingLot with hardcoded dependencies cannot be injected with test doubles. The Singleton is the wrong choice here and should be stated outright.
The four slot types, CompactSlot, LargeSlot, HandicappedSlot, and ElectricSlot, share the ParkingSlot interface and differ only in which vehicles they accept.
The decision being probed is how canFit works internally. Three options:
if (vehicle.type === MOTORCYCLE) return true couples slot logic to every vehicle type. Every new vehicle type requires editing every slot subclass. Rejected."CompactSlot.canFit returns true for Motorcycles and Cars. LargeSlot returns true for Cars and Trucks. HandicappedSlot returns true only when the vehicle carries a handicapped permit. ElectricSlot returns true for Electric vehicles only. Each subclass overrides canFit with its own logic."
"If the product adds a new Oversized vehicle type next quarter, how many classes do you touch?"
A Mid with a type switch inside canFit touches every slot subclass to add the new vehicle type. The boundary is recognizing that the type switch approach violates open closed: adding a vehicle type requires editing existing classes.
"Rather than switch on vehicle.type inside canFit, I'd have vehicles declare their capabilities. A Car has STANDARD, an Electric car has STANDARD plus EV_CHARGING, a handicapped registered car has STANDARD plus HANDICAPPED_PERMIT. Each slot declares its required capabilities. canFit just checks whether the vehicle's capability set is a superset of the slot's requirements. Adding a new vehicle type is defining a new capability set. Adding a new slot type is declaring its required set. Nothing existing changes."
"A slot physically fits only small vehicles but also requires EV charging. How does the capability model express a size constraint alongside a capability constraint?"
Senior is expected to note that physical size is a separate axis from capability: add a size enum to Vehicle and have the slot declare both a size range and a required capability set. Conflating size with capabilities is the boundary.
"I'd push back on a deep slot inheritance tree if the real differentiator is just configuration. Max vehicle size and required capabilities are both data. A single SlotImpl class with a SlotConfig value object, a size category and a capability set injected at construction, covers all four types. The subclass hierarchy earns its existence when behavior diverges beyond canFit, for example if ElectricSlot has state around charging sessions. If the only behavioral difference is what canFit returns, the hierarchy is adding classes to carry data, not to vary behavior, which is a design smell worth naming."
"How does the composition approach affect how you'd store slot configurations in the database?"
Staff is expected to challenge the inheritance model when it is purely data driven, and to connect the design to persistence: a config driven slot is a row in a slot_types table. A class per type requires an application deployment every time a new slot variant is introduced.
The question "where does the fee logic live?" is the highest leverage design question in this problem. Three options:
ticket.calculateFee() means Ticket now knows about pricing logic. When the business adds a monthly pass discount or a surge multiplier, the Ticket class changes. Ticket has two reasons to change: representing the parking event and computing its cost. Rejected.feeCalculator.calculate(ticket). HourlyFeeCalculator, FlatRateFeeCalculator, and DynamicFeeCalculator each implement the interface. Swapping pricing rules is injecting a different implementation. Adding a new rule is adding a new class. Nothing else changes.The fee formula for the hourly model: fee = ceil(durationHours) × ratePerHour. At $3 per hour with a 2.5-hour stay, fee = ceil(2.5) × 3 = $9. The ceiling rather than round is standard because partial hours bill as full hours. A candidate who says "multiply duration by rate" without noting the ceiling function has skipped a real product rule.
"I'd have a FeeCalculator class separate from Ticket, because Ticket is a record of the parking event and FeeCalculator is business logic. ParkingLot calls the calculator on exit and attaches the result to the Receipt."
"How do you switch between hourly and flat rate fee structures at runtime?"
A Mid who creates a separate class but makes it a static utility with hardcoded logic has not separated the concern. It is still coupled to the specific rule. The boundary is recognizing that FeeCalculator needs to be an interface with interchangeable implementations.
"This is the Strategy pattern: FeeCalculator is an interface with a single calculate(ticket) method. HourlyFeeCalculator, FlatRateFeeCalculator, and DynamicFeeCalculator each implement it. ParkingLot holds a reference to the active FeeCalculator, injected at construction or set by an admin operation when the pricing rule changes. Adding a weekend surge calculator is a new class and it does not touch ParkingLot or Ticket. I'd also make sure calculate returns a Money type, not a double, to avoid floating point currency bugs. Fees are exact amounts, not approximate ones."
"The active calculator changes at midnight on a Friday. A car that entered before midnight exits after. Which calculator applies?"
Senior is expected to name Strategy explicitly, flag the Money type issue, and handle the in-flight ticket problem: stamp the rate parameters onto the Ticket at entry time so the exit computation uses the rate that was active when the car entered, not the current one.
"Beyond the mechanical pattern: the right question about any fee change is whether a car that entered under the old rate should bill under the new one. For most operators the answer is no, so I'd stamp the rate parameters onto the Ticket at entry, not a reference to the current FeeCalculator but the actual rate values, so the exit computation is self-contained even if the calculator changes between entry and exit. This also means the FeeCalculator at exit is a pure function of the ticket data, which makes billing auditable: every bill is reconstructable from the ticket alone."
"You are stamping rate parameters at entry. What happens when the fee formula itself changes, not just the rate value?"
Staff is expected to separate the immutable record (which rate applied at entry) from the mutable strategy (which rules are live today), and to reason about auditability. A formula change is a new FeeCalculator version. Old tickets retain a reference to the version in effect at entry, or the stamped parameters encode enough to reconstruct the old formula.
This section separates Mid from Senior more reliably than any class diagram. The scenario: two vehicles arrive at the same instant and both call findAvailableSlot, which returns the same slot ID. Both proceed to assign that slot. Now two cars are trying to park in one space.
The naive model, read an available slot then write the vehicle reference, is a classic time of check to time of use race condition (TOCTOU). Three mitigations in order of increasing robustness:
assignedAt version field. Assignment is a conditional update: UPDATE slots SET vehicle = ? WHERE slotId = ? AND vehicle IS NULL. The database rejects a second writer and the application retries with the next available slot. Works well when multiple app instances manage the same lot.For an LLD interview, the synchronized method is the correct answer. The interviewer wants to know that you named the race condition and chose the simplest correct mitigation for the actual load.
"If two vehicles arrive at the same time there is a race condition: both could get assigned the same slot. I'd make the find and assign operation atomic by putting a lock on the method so no two callers can run it on the same floor at the same time."
"Where exactly does the lock live and what does it protect?"
A Mid who names the race condition unprompted has passed the floor. The boundary is whether they can say where the lock belongs: on ParkingFloor.findAndAssignSlot() as a synchronized method, not on the individual slot, because the critical section covers the scan and claim together.
"The race is a classic TOCTOU: findAvailableSlot returns slot 42 as free, then both callers write to slot 42. The synchronized approach is correct and has negligible cost at parking lot entry rates. 30 entries per hour on a floor means contention is near zero. I'd prefer the synchronized method over optimistic locking precisely because it is simpler and the load does not justify a retry loop. If this were a high throughput system where hundreds of entries per second contend on a floor, a database conditional update is the right move. But I would not prematurely optimise for a load this system will never see."
"Your synchronized method is on ParkingFloor. What happens if two app server instances are managing the same lot?"
Senior is expected to compare mitigations with a load argument, not just pick one. The multi-instance follow-up is the expected next probe: a JVM-level synchronized method does not span processes, so either the architecture routes all entries for a floor to one process, or concurrency control moves to the database layer.
"The right level for concurrency control depends on the deployment model, and that is a conversation worth having before picking a mechanism. If this is a single server application, which is reasonable for a lot of up to a few thousand slots, a synchronized method per floor is correct, simple to reason about, and zero operational overhead. If it is a distributed deployment with multiple app servers managing the same lot, the lock must move to the database layer: a conditional UPDATE returning the claimed row, or a slot reservation table with a unique constraint on (slotId, occupiedAt). I would name that choice explicitly rather than pick one mechanism and wave at the other case, because the deployment model is an architectural decision that belongs in scope."
"Walk me through the SQL for the database layer slot assignment. What does the application do when the update returns zero rows?"
Staff is expected to connect the concurrency mechanism to the deployment model as a first-class constraint, and to sketch the SQL: UPDATE slots SET vehicle_id = ?, assigned_at = NOW() WHERE id = ? AND vehicle_id IS NULL returning the updated row, with an application-level retry to the next available slot on zero rows returned.
The extensibility questions in a parking lot LLD are not hypothetical. Every real parking system eventually adds EV charging, reserved slots, monthly passes, and multi-lot management. The design should anticipate these at the interface level without implementing them prematurely.
Three extensions worth discussing explicitly:
EV charging as a slot state: an ElectricSlot is not just a slot that accepts EVs. It has a charging session with its own start time, energy consumed, and a per kWh billing component. The fee is a combination of time parked and energy delivered. This only works cleanly if FeeCalculator is already an interface. A FeeCalculator that contains an if-else for slot type is broken before the EV extension arrives.
Reserved slots: a ReservedSlot has a booking window. canFit must check not just vehicle type but also whether the booking is active for the arriving vehicle. The capability model from section 3 handles this by adding a RESERVATION capability to the vehicle record, without adding branches to existing slot classes.
Multi-lot management: ParkingLot becomes a node in a ParkingNetwork. The ParkingNetwork's findAvailableSlot() calls each lot's findAvailableSlot() and selects the nearest result. This is transparent to the floor and slot classes because they never needed to know about the network. The ParkingLot interface abstraction from section 2 is what makes this composition possible.
"Adding a new vehicle type means adding a new Vehicle subclass and updating the canFit implementations in the relevant slot classes. Adding a new fee structure means adding a new FeeCalculator implementation. Neither change touches the core flow."
"How would you add monthly pass billing, a flat monthly fee instead of a per entry charge, to this design?"
A Mid who can describe how to extend vehicle types or fee rules without editing existing classes has demonstrated open closed at a basic level. The boundary is whether they can do the same for a more complex extension like monthly passes or multi-lot routing.
"Monthly pass: a MonthlyPassCalculator implements FeeCalculator and checks the vehicle's pass status against the active billing period. It returns zero fee for covered entries. ParkingLot.exit() calls it like any other calculator and does not know the pass logic exists. For EV charging: ElectricSlot tracks a ChargingSession with start time and kWh delivered. The fee is split between time and energy, so I'd use a CompositeCalculator that sums a time component and an energy component. The interface is the same. The data available to it is richer."
"CompositeCalculator that delegates to two sub-calculators. What pattern is that?"
Senior is expected to describe concrete extensions with the interface changes named, not just say the design is extensible. The Composite pattern answer is a depth signal: a CompositeCalculator that holds a list of sub-calculators and sums their results is literally the Composite design pattern applied to Strategy implementations.
"The extensibility principle I am optimising for is: the blast radius of any single change should be exactly one class. I'd also add the Observer pattern for the display board. ParkingLot emits SlotAssigned and SlotReleased events. DisplayBoard, AlertService, and CapacityLogger each subscribe. Adding a real time mobile app notification is a new subscriber class, not an edit to ParkingLot.parkVehicle(). The concurrency mechanism, the fee strategy, the slot matcher, and the event emitter are all injected rather than constructed internally, so every behavior variant is testable in isolation. The design is done when you can change any single behavior by adding one class and injecting it."
"Observer pattern in a single process parking system versus a distributed one. What changes in the implementation?"
Staff is expected to arrive at the Observer pattern for side effects (display, notifications, logging) and to frame the extensibility section as blast radius minimisation. The distributed follow-up probes whether they know when in-process observers (method calls on a list of listeners) give way to message queues (Kafka, SQS) when the system spans processes.
The parking lot problem runs through every LLD dimension: requirements clarity, class design, design patterns, and extensibility. Almost none of the score lives in whether you remembered HandicappedSlot or got the class names right. The score lives in whether you opened with the three axes of change before drawing anything, kept fee logic off the Ticket class, explained the canFit direction, named the TOCTOU race and chose the simplest correct fix for the actual load, and described at least two concrete extensions with the interface change each one requires.
The candidates who struggle are usually the ones who knew all of this but rehearsed by drawing diagrams in silence. An LLD interview is a live design conversation. The follow-up digs in each section above are the actual questions, and the ability to answer them while sketching and talking simultaneously is a distinct skill from knowing the answer cold. That gap is what mock practice trains and solo study cannot.