This gives away every stage. For a clean attempt, practise it first, then come back.
The short version
- Start with requirements, not classes. Every class in the final design can be traced back to a sentence the interviewer said.
- Nouns become candidate classes, but only the ones with their own data and their own rules survive. Bikes, cars and trucks become one enum, not three classes.
- Give each job to the class that already has the information it needs. That one rule settles most “who does what” questions.
- Design top-down, from what the gate calls, inward. Code bottom-up, from the smallest piece that must be correct, outward.
- Walk the entry and exit flows call by call before coding. That’s where you find missing pieces and races.
- Two races matter: two cars for one spot, one ticket at two exits. Both are fixed by claiming atomically, not by one big lock.
- Follow-ups (charging, weekend pricing, lost tickets) are answered by adding or wrapping classes, never by editing tested ones.
How this walkthrough is built
In a low-level design (LLD) round, you get about an hour to turn a real-world thing into classes and working code. Nobody expects a perfect system. The interviewer wants to see a process: how you ask, how you break the problem down, how you decide who does what, and whether your code is safe and easy to change.
So this walkthrough doesn’t show you a finished design and explain it. It builds the design in the same order you would in an interview, and every step uses only what the steps before it produced:
| Step | What you do | What comes out of it |
|---|---|---|
| 1. Understand the problem | Ask questions, write down requirements | A short list of what the system must do, and how well |
| 2. Break it into entities | Nouns → classes, verbs → methods, find what changes | A list of classes, each with one clear job |
| 3. Choose a direction | Top-down for designing, bottom-up for coding | The order you’ll work in |
| 4. Design each class | Decide what each class knows and does | A “card” per class |
| 5. Connect the classes | Pick the right kind of relationship | Who owns whom, who uses whom |
| 6. Draw the diagram | Build it up in three versions | The complete class diagram |
| 7. Name the patterns | Spot where the requirements asked for one | Strategy, Observer, Facade, and why not Singleton |
| 8. Walk the flows | Trace entry and exit call by call | Gaps found while they’re cheap to fix |
| 9. Write the code | Bottom-up, one class at a time | Code that compiles and runs |
| 10. Make it safe | Find shared state, fix the races | Two atomic claims and a test that proves them |
| 11. Extend it | Answer follow-ups | New classes, no edits to old ones |
If you only remember one thing from this page, make it this: the classes come out of the requirements. You don’t invent them. Every class we end up with can be traced back to a sentence the interviewer said.
Step 1Understand the problem
Picture the real thing first
Before any classes, think about what actually happens at a mall parking lot. You drive up to a barrier. A machine prints a ticket with the time on it, and the barrier lifts. You drive around looking for a space your car fits in. Some floors have big spaces for vans. A sign at each floor’s ramp says how many spaces are free. Hours later you drive to the exit, put your ticket in a machine, it tells you how much you owe, you pay, and the barrier lifts.
Everything in that story is a hint. Barrier, ticket, time, space, fits, floor, sign, how much, pay. We’ll come back to these words in step 2. For now, notice that the story is vague in a dozen places: Does the system pick your spot, or do you? How is the price worked out? What if two cars arrive at once? That vagueness is deliberate. Asking about it is the first thing being tested.
Ask before you design
Group your questions by area so you don’t forget one: the building, the vehicles, getting in and out, money, what’s shown, and what happens at the same time.
Interviewer: Design a parking lot system.
You: Sure. Let me ask a few questions first. Is this one building with several floors, or several separate lots?
Interviewer: One building, several floors, each with many spots.
You: What kinds of vehicles do we handle, and are the spots different sizes?
Interviewer: Bikes, cars and trucks. Spots are small, medium and large.
You: Can a vehicle park in a spot bigger than it needs?
Interviewer: Yes. A bike can use any spot, a car can use medium or large, a truck needs large.
You: Does the system assign the spot, or does the driver choose?
Interviewer: The system assigns it, and prints it on the ticket.
You: How do people get in and out?
Interviewer: Through entry gates and exit gates. There are several of each, all working at the same time.
You: How is the fee worked out?
Interviewer: By the hour, with a different rate for each spot size. The first ten minutes are free.
You: Will those pricing rules change?
Interviewer: Definitely. The mall changes them every few months.
You: How do people pay?
Interviewer: Cash, card or UPI.
You: Should anything show how many spots are free?
Interviewer: Yes, each floor has a display board at the ramp.
You: What about reservations, electric charging, lost tickets?
Interviewer: Not for now. We might come back to them.
That’s eleven questions in about four minutes. Each one changed something:
| Question | What the answer decided |
|---|---|
| Several floors? | There’s a layer between “the lot” and “a spot”: the floor. |
| Spot sizes? | Size is a core idea. Vehicles and spots both need it. |
| Bigger spot allowed? | “Fits” is a comparison (at least as big), not a fixed pairing. |
| Who picks the spot? | The system needs a rule for choosing. Rules can change. |
| Several gates at once? | Concurrency. Two gates might want the same spot at the same instant. |
| Pricing will change? | Pricing must be swappable without editing the rest. |
| Cash, card, UPI? | Payment talks to outside systems, so it sits behind a boundary. |
| Display boards? | Something must react when spots change. |
| Out-of-scope items | Saying them out loud shows you thought about them, and sets up the follow-ups. |
Functional requirements
Functional requirements are what the system does. Number them, because we’ll check the finished design against each one.
- FR1. The lot has several floors. Each floor has small, medium and large spots.
- FR2. A vehicle (bike, car or truck) can park in any free spot that’s big enough for it.
- FR3. At an entry gate, the system picks a spot and issues a ticket with the spot and the time.
- FR4. At an exit gate, the system works out the fee from the time and the spot size.
- FR5. The driver pays by cash, card or UPI. The spot is then freed.
- FR6. Each floor’s display board shows how many spots of each size are free.
- FR7. The same vehicle can’t be parked twice at once, and a used ticket can’t be used again.
FR7 wasn’t said by the interviewer. It’s a rule you’d expect any real lot to have, so add it and say you added it. Interviewers like candidates who spot the obvious rules nobody mentioned.
Non-functional requirements
Non-functional requirements are how well the system does it. In LLD, they’re usually about the code itself rather than speed or scale.
- NFR1: Easy to extend. New vehicle types, allocation rules and pricing rules should mean adding code, not editing working code.
- NFR2: Correct under concurrency. Several gates run at once. Two cars must never get the same spot, and one ticket must never be paid twice.
- NFR3: Testable. We should be able to test “a car parked for 75 minutes” without waiting 75 minutes, and test payments without charging a real card.
- NFR4: Money is exact. Fees are never off by a paisa.
Each of these will show up as a specific decision later. NFR1 becomes interfaces. NFR2 becomes atomic operations. NFR3 becomes passing in a clock and a payment processor. NFR4 becomes storing money as whole paise instead of decimals.
Assumptions we’ll write down
Some details the interviewer won’t care about, but you still have to choose. Choose sensibly, write them down, and move on:
| Assumption | Why it’s reasonable |
|---|---|
| The spot is assigned automatically at the gate | The interviewer said so |
| Best fit: use the smallest size that fits, then the lowest floor | Keeps large spots free for trucks, which can’t use anything else |
| A stay of 10 minutes or less is free; after that, hours are rounded up and the whole stay is charged | Matches how most Indian malls charge |
| Rates: small ₹20/hour, medium ₹40/hour, large ₹80/hour | Example numbers so we can test |
| Number-plate reading is done by the gate hardware | It’s outside our software; we receive a plate as text |
| Everything lives in memory for now | Saving to a database is a follow-up (step 11) |
Step 2Break it into entities
This is the step most people skip, and it’s the one that matters most. Skipping it is like starting to build a house before deciding how many rooms it has. It takes about seven minutes and saves twenty.
Pull out the nouns
Read the requirements back and underline every noun: lot, floor, spot, size, vehicle, bike, car, truck, gate, entry gate, exit gate, ticket, time, fee, rate, payment, cash, card, UPI, display board, driver, plate, mall.
Each noun is a candidate class, not a class. Now we filter.
Three questions that decide if a noun is a class
For each noun, ask:
- Does it have its own data that changes over time, or that identifies it?
- Does it have its own rules, things it must check or protect?
- Will it change independently of everything else, so it deserves its own place in the code?
A noun that gets a “yes” to at least one usually becomes a class or interface. A noun that’s just a fixed set of values becomes an enum. A noun that’s just a number or text becomes a field. A noun that’s outside the software becomes nothing at all.
| Noun | Own data? | Own rules? | Changes alone? | Becomes |
|---|---|---|---|---|
| Lot | floors, active tickets | no car parked twice | — | ParkingLot class |
| Floor | its spots, free counts | finds a free spot of a size | — | ParkingFloor class |
| Spot | id, size, who’s in it | only one vehicle at a time | — | ParkingSpot class |
| Size | — | — | — | SpotSize enum: three fixed values |
| Vehicle | plate, type | — | — | Vehicle record (plain data) |
| Bike, car, truck | — | only “which size do I need” | — | values of a VehicleType enum (see the trap below) |
| Ticket | id, vehicle, spot, entry time | — | — | Ticket record |
| Fee, rate | — | how a fee is worked out | yes, often | PricingStrategy interface |
| Payment | — | — | yes (new providers) | PaymentProcessor interface |
| Cash, card, UPI | — | — | — | PaymentMode enum |
| Entry gate, exit gate | gate id | — | — | thin EntryGate, ExitGate classes |
| Display board | numbers it shows | — | yes (later: an app) | DisplayBoard, behind a FloorListener interface |
| Time | — | — | — | a field on the ticket (Instant) |
| Plate | — | — | — | a field on the vehicle |
| Driver, mall | — | — | — | nothing: outside the software |
Two nouns were quietly missing from the requirements, and you only notice them while filling this table. Who chooses the spot? The lot could, but the interviewer said the rule might change, so it gets its own interface: SpotAllocationStrategy. And who does the display board listen to? That turns into FloorListener. Finding missing nouns is a normal and good part of this step.
The last row is the classic trap. Driver, Customer, Attendant and Building feel important, so people add them. But nothing in our requirements needs a driver to have data or rules. The system sees a vehicle with a plate, not a person. Adding them makes the design bigger without making it better, and the interviewer will ask what they’re for.
The vehicle trap
This decision deserves its own section, because it’s the one most candidates get wrong.
The tempting design is one class per vehicle type:
abstract class Vehicle { String plate; }
class Bike extends Vehicle { }
class Car extends Vehicle { }
class Truck extends Vehicle { }
// ...and then, somewhere in the spot-finding code:
boolean canPark(Vehicle v, ParkingSpot s) {
if (v instanceof Bike) return true;
if (v instanceof Car) return s instanceof MediumSpot || s instanceof LargeSpot; 1
if (v instanceof Truck) return s instanceof LargeSpot;
return false;
}- 1type checks everywhere
It looks object-oriented, but look at what the subclasses actually do. Nothing. Bike, Car and Truck have no methods of their own. The only difference between them is which size of spot they need, and that difference lives outside them, in a chain of instanceof checks. Adding a van means editing that chain, and every other chain like it.
Ask the question directly: what’s different about a bike and a truck in this system? Only the size they need. Different data, not different behaviour. So model exactly that:
enum SpotSize { SMALL, MEDIUM, LARGE } 1
enum VehicleType {
BIKE(SpotSize.SMALL), CAR(SpotSize.MEDIUM), TRUCK(SpotSize.LARGE);
private final SpotSize needs;
VehicleType(SpotSize needs) { this.needs = needs; }
SpotSize needs() { return needs; }
boolean fits(SpotSize spot) { return spot.compareTo(needs) >= 0; } 2
}
record Vehicle(String plate, VehicleType type) { }- 1listed smallest to largest, so sizes can be compared
- 2any spot at least as big
Adding a van is now one line: VAN(SpotSize.LARGE). No if statements anywhere ask “what kind of vehicle is this?”.
The rule to say out loud in the interview: inheritance is for different behaviour. When only the data differs, use a field or an enum. If the interviewer later says “trucks must be inspected before parking”, then trucks have behaviour of their own, and a class might earn its place. Not before.
Pull out the verbs
Now the verbs in the requirements: park, pick a spot, issue a ticket, work out the fee, pay, free the spot, show free spots, refuse a duplicate.
Group them into use cases: things someone outside the system wants to do.
| Use case | Who starts it | Verbs inside it |
|---|---|---|
| UC1: Park | Entry gate | check the plate isn’t inside, pick a spot, take the spot, issue a ticket, update the board |
| UC2: Leave | Exit gate | check the ticket, work out the fee, take payment, free the spot, update the board |
| UC3: See free spots | Display board | count free spots per size per floor |
Three use cases. That’s the whole public surface of the system. Everything else is internal.
Give each job to the class that knows the most
Now the most important question in all of LLD: who does each job?
The rule that answers it is called Information Expert: give a job to the class that already has the information needed to do it. Don’t move data to the work; move the work to the data.
| Job | Information needed | Who has it | So the owner is |
|---|---|---|---|
| Is this spot free? Take it. Free it. | who’s in the spot | the spot | ParkingSpot |
| Find a free spot of a size on this floor | the floor’s list of spots | the floor | ParkingFloor |
| Count free spots per size | every take and free on the floor | the floor | ParkingFloor |
| Does this vehicle fit this size? | what size the vehicle needs | the vehicle type | VehicleType |
| Which floor and size to try first? | all floors, plus a policy | nobody, yet: it’s a rule that will change | SpotAllocationStrategy |
| How much does this stay cost? | entry time, exit time, spot size, a policy | the ticket has the facts; the policy will change | PricingStrategy |
| Charge the money | the bank, the UPI provider | outside systems | PaymentProcessor |
| Is this plate already parked? Is this ticket valid? | all active tickets | only the lot sees all of them | ParkingLot |
Look at what happened to ParkingLot. In a weak design it does everything: finds spots, counts, prices, charges. Here it only does the one thing nobody else can, keeping track of active tickets, and coordinates the others. That’s a much smaller, safer class.
CRC cards
A quick way to sketch this on a whiteboard is a CRC card for each class: Class, Responsibilities, Collaborators (the classes it works with). Writing them takes a minute and catches classes that do too much (too many responsibilities) or know too much (too many collaborators).
| Class | Responsibilities | Collaborators |
|---|---|---|
ParkingSpot | take a vehicle safely; free itself; report its size | Vehicle |
ParkingFloor | find and take a free spot of a size; free spots; keep counts; tell listeners | ParkingSpot, FloorListener |
SpotAllocationStrategy | choose which floor and size to try, in what order | ParkingFloor, VehicleType |
PricingStrategy | work out a fee for a ticket and an exit time | Ticket |
PaymentProcessor | charge an amount by a payment mode | outside systems |
ParkingLot | park and unpark; reject duplicates; remember active tickets | all of the above |
EntryGate, ExitGate | pass requests from the hardware to the lot | ParkingLot |
DisplayBoard | show free counts when told they changed | — |
Every card has one or two lines. When a card needs five lines, the class probably needs splitting.
Find what will change
Last part of this step: look for the parts the interviewer hinted will change, or that obviously could. Each one goes behind an interface, so a new version can be added without touching the code that uses it.
| Will change | Hint from the interviewer | Becomes |
|---|---|---|
| How a spot is chosen | “the system picks the spot” (by what rule?) | SpotAllocationStrategy interface |
| How parking is priced | “the mall changes them every few months” | PricingStrategy interface |
| How money is taken | “cash, card or UPI” | PaymentProcessor interface |
| Who wants to know when spots change | “display boards” (and one day, an app) | FloorListener interface |
And just as important, things that won’t change and so get no interface: spots, floors, tickets. Don’t add an interface “just in case”. An interface that has one implementation forever is clutter.
Step 3Top-down or bottom-up?
We have our pieces. There are two ways to put them together, and strong engineers use both, at different moments.
Top-down starts from the outside, from what the world calls, and asks “what does this need?” over and over:
- The entry gate needs to call
park(vehicle)and get a ticket back. SoParkingLot.park()exists. - To park, the lot needs to choose a spot. So it needs a
SpotAllocationStrategywithallocate(floors, vehicle). - To allocate, the strategy needs floors that can hand over a free spot of a given size. So
ParkingFloor.claim(vehicle, size)exists. - To claim, the floor needs spots that can be taken without two cars getting the same one. So
ParkingSpot.tryOccupy(vehicle)exists.
Each class and method exists because the one above needed it. Nothing is built that nobody uses.
Bottom-up goes the other way. It starts from the smallest pieces and finishes each one before building on it: first SpotSize and VehicleType, then a ParkingSpot that can be taken safely, then a ParkingFloor made of spots, then strategies, then the ParkingLot.
Watch the same design come together in both orders:
| Top-down | Bottom-up | |
|---|---|---|
| Starts from | What callers need: the public methods | The smallest building blocks |
| Strength | Nothing useless gets built. The interviewer sees your API early. | Each piece is finished and testable before anything depends on it. |
| Risk | The tricky low-level parts (like taking a spot safely) stay vague until late. | You can build pieces that don’t fit together, and the big picture appears late. |
| Best for | Designing: the class diagram and the flows | Coding: writing classes in an order where each one already works |
So the approach for this problem, and most LLD problems, is: design top-down, code bottom-up. Draw the diagram and walk the flows from the gate inward (steps 4 to 8), so the design is driven by real needs. Then write the code from the spot outward (step 9), so every class you write sits on finished, correct pieces.
Tell the interviewer you’re doing this. “I’ll design from the gate inward, then code from the spot outward” takes five seconds to say, and it tells them you have a plan.
Step 4Design each class
Now we take each entity from step 2 and decide exactly what it knows and what it does. For each one: its job in one sentence, what it knows (its fields), what it does (its methods), what it deliberately doesn’t do, and the decisions behind it.
We go in top-down order here, because that’s how you’d explain it. The code in step 9 goes the other way.
ParkingLot: the front door
Job: the single entry point. Park a vehicle, unpark a vehicle, and keep track of who’s inside.
| Knows | Does | Doesn’t do |
|---|---|---|
| its floors | park(vehicle) → Ticket | choose spots (asks the strategy) |
| an allocation strategy | unpark(ticketId, mode) → Receipt | work out fees (asks pricing) |
| a pricing strategy | activeTicketFor(plate) | talk to banks (asks payments) |
| a payment processor | count free spots (floors do) | |
| a clock | ||
| active tickets, by ticket id and by plate |
Notice how much the “doesn’t do” column holds. That’s the point. ParkingLot is a facade: a simple front door that hides a group of classes behind two methods. The gates don’t need to know floors or strategies exist.
Why does it keep two maps of active tickets? By ticket id, because the exit gate arrives with a ticket. By plate, to enforce FR7 (the same car can’t be parked twice). That second map will also make the lost-ticket follow-up almost free, as we’ll see in step 11.
Why is the clock passed in instead of calling the system time? NFR3. A test can hand in a clock it controls and move time forward by 75 minutes instantly.
ParkingFloor: spots grouped by size
Job: own a floor’s spots, hand out a free one of a given size, take spots back, and keep count.
| Knows | Does | Doesn’t do |
|---|---|---|
| its level number | claim(vehicle, size) → spot or nothing | decide which size to try (the strategy does) |
| its spots, grouped by size | release(spot) | know what a display board is |
| free count per size | freeCount(size) | |
| its listeners | subscribe(listener) |
Two decisions here. First, spots are grouped by size in a map. A car looking for a medium spot never scans the bike spots. Second, the floor keeps a running count of free spots per size instead of counting every time someone asks. Display boards ask constantly; counting 500 spots each time would be wasteful, and “is anything free here?” becomes instant.
The floor tells listeners when counts change, without knowing who they are. That’s the Observer pattern, and step 7 explains why it’s better than the floor calling the display board directly.
ParkingSpot: the one thing that must never be wrong
Job: hold at most one vehicle, and make taking it safe even when two gates try at once.
| Knows | Does | Doesn’t do |
|---|---|---|
its id (like F1-M3) | tryOccupy(vehicle) → true/false | decide whether a vehicle fits (the vehicle type does) |
| its size | release() | |
| who’s in it, or nobody | isFree() |
The most important method in the whole design is tryOccupy, and its name is deliberate. It’s not occupy(), because it can fail. Two gates might both find this spot free and both try to take it. Exactly one must succeed. The method returns true to the winner and false to the loser, who then tries another spot. Step 10 shows exactly how.
Why not a separate isFree() check followed by occupy()? Because between the check and the take, another gate can take the spot. That gap is the bug that most parking lot answers contain. Taking must be one indivisible step.
Vehicle, VehicleType and SpotSize: the vocabulary
Job: describe what a vehicle is and what it needs.
SpotSize is an enum with three values, listed smallest to largest so they can be compared. VehicleType is an enum where each value knows the size it needs and can answer fits(size). Vehicle is a record: a small class that just holds data (plate and type) and can’t be changed after it’s made.
The “fits” rule belongs to VehicleType, by Information Expert: the vehicle type knows what it needs. Not the spot, not the floor, not the lot.
Ticket and SpotAssignment: the record of a stay
Job: remember everything about one stay that the exit will need.
A Ticket holds its id, the vehicle, the spot, the floor and the entry time. It’s a record: once printed, a ticket never changes.
Why does the ticket hold the floor as well as the spot? We’ll discover the answer in step 8, while walking the exit flow: freeing a spot must update that floor’s free count, so the exit needs to know the floor. This is why the strategy returns a small SpotAssignment (floor plus spot) instead of just a spot.
SpotAllocationStrategy: the rule for choosing
Job: given the floors and a vehicle, find and take a spot, using some rule.
It’s an interface with one method, allocate(floors, vehicle), which returns a SpotAssignment or nothing if the lot is full. Our first rule, BestFitLowestFloor, tries the smallest size that fits on every floor before trying a bigger size. Later rules (“spread cars evenly”, “closest to the lifts”) are new classes.
Why does the strategy take the spot, rather than just suggest one? Because suggesting and then taking leaves a gap where another gate can take it first. The strategy asks a floor to claim, and the claim is atomic. Choosing and taking happen together.
PricingStrategy: the rule for charging
Job: given a ticket and an exit time, return the fee.
An interface with one method, fee(ticket, exitTime), returning paise as a whole number (long). Our first rule is HourlyPricing: free for the first ten minutes, then a rate per hour by spot size, with hours rounded up.
Why is money a long in paise and not a double in rupees? NFR4. Decimals in computers can’t store most fractions exactly: 0.1 + 0.2 isn’t exactly 0.3. Over thousands of payments those tiny errors add up. Whole paise never go wrong.
PaymentProcessor and PaymentMode: the money boundary
Job: charge an amount using a payment mode, and say whether it worked.
PaymentMode is an enum (cash, card, UPI). PaymentProcessor is an interface with charge(amount, mode) → true/false. The real version would talk to a bank or a UPI provider; a test version just returns true (or false, to test failures). The lot never knows which.
EntryGate and ExitGate: thin on purpose
Job: receive a request from the gate hardware and pass it to the lot.
They hold a gate id and a reference to the lot, and have one or two methods each (arrived, leaving, and later leavingWithoutTicket). They look almost too simple to be classes. They earn their place because they keep “how a vehicle arrives” out of the lot. The day the mall adds a mobile app, it calls the same lot methods, and the lot doesn’t change.
FloorListener and DisplayBoard: reacting to change
Job: FloorListener is an interface with one method, freeSpotsChanged(level, size, free). DisplayBoard implements it and shows the numbers.
The floor holds a list of listeners and tells all of them whenever a count changes. It doesn’t know if a listener is a board at the ramp, a mobile app, or a logger.
Step 5How the classes relate
Classes connect in a few different ways, and choosing the right one says something real about the design. Here are the five you need, each with an example from this problem:
| Relationship | Meaning | In plain words | In this design |
|---|---|---|---|
| Composition (filled diamond) | owns, and the part can’t exist alone | “made of” | lot made of floors; floor made of spots |
| Aggregation (empty diamond) | holds, but the part lives on its own | “keeps a list of” | lot keeps active tickets; a ticket exists before and after |
| Association (plain arrow) | uses, holds a reference | “knows about” | ticket knows its spot; gate knows the lot |
| Realization (dashed, hollow arrow) | implements an interface | “is a kind of” | HourlyPricing is a PricingStrategy |
| Dependency (dashed arrow) | uses briefly, as a parameter | “uses for a moment” | strategy uses a Vehicle passed in |
The difference between composition and aggregation trips people up. Ask: if the owner disappears, does the part disappear too? Delete a floor and its spots are gone; they have no meaning without it. That’s composition. But a ticket that’s handed to the driver outlives the lot’s map of active tickets. That’s aggregation.
Multiplicity says how many: a lot has 1..* floors (one or more), a floor has 1..* spots, a ticket has exactly one spot, a spot has at most one active ticket.
Step 6The class diagram, built up
Don’t draw the whole diagram in one go. Build it in three versions, out loud. The interviewer can follow along, and it’s much easier to change a small diagram than a big one.
Version 1: what the lot is made of
Start with structure. A lot is made of floors; a floor is made of spots. A ticket refers to a spot.
Version 2: the parts that change
Now add the interfaces from “find what will change”. ParkingLot doesn’t decide which spot or how much; it asks. Each interface gets its first implementation.
Version 3: the edges, and the whole picture
Finally the gates, which call the lot, and the display boards, which listen to floors. This is the complete design:
Check it against the requirements
Before going further, trace every requirement to a class and method. If one has no home, the design is incomplete. If a class serves no requirement, it shouldn’t be there.
| Requirement | Where it lives |
|---|---|
| FR1: floors with three spot sizes | ParkingLot → ParkingFloor → ParkingSpot, SpotSize |
| FR2: park in any spot big enough | VehicleType.fits, used by BestFitLowestFloor |
| FR3: pick a spot, issue a ticket at entry | EntryGate.arrived → ParkingLot.park |
| FR4: fee by time and size | PricingStrategy.fee, using the ticket’s entry time and spot size |
| FR5: pay by cash, card, UPI, then free the spot | ParkingLot.unpark → PaymentProcessor.charge → ParkingFloor.release |
| FR6: display boards | ParkingFloor notifies FloorListener → DisplayBoard |
| FR7: no duplicate parking, no ticket reuse | ParkingLot‘s two maps |
| NFR1: easy to extend | four interfaces: allocation, pricing, payments, listeners |
| NFR2: correct under concurrency | ParkingSpot.tryOccupy, atomic ticket removal (step 10) |
| NFR3: testable | Clock and PaymentProcessor passed in |
| NFR4: exact money | fees as long paise |
Every row has a home, and every class appears in at least one row.
Step 7The patterns, and why each one is here
Design patterns are named solutions to common problems. The wrong way to use them is to decide “I’ll use Strategy” and then look for a place. The right way is what happened above: a requirement creates a problem, and a pattern turns out to be the clean answer. Then you name it, because naming it tells the interviewer you know the trade-offs.
| Pattern | Where | The problem it solved | Without it |
|---|---|---|---|
| Strategy | SpotAllocationStrategy, PricingStrategy, PaymentProcessor | Rules that change often must be swappable | if (mode == WEEKEND) branches growing inside the lot |
| Observer | FloorListener, DisplayBoard | Several things react to spot changes, and the list will grow | The floor calls boards directly and must change for each new screen |
| Facade | ParkingLot | Gates need one simple entry point to a group of classes | Gates juggle floors, strategies and payments themselves |
| Value objects | Vehicle, Ticket, Receipt, SpotAssignment | Data that’s passed around should never change by surprise | Some code edits a ticket’s entry time and fees go wrong |
Why ParkingLot is not a Singleton
Many answers make ParkingLot a Singleton: a class that allows exactly one object, reached from anywhere with something like ParkingLot.getInstance(). The reasoning sounds fine: “there’s only one lot.”
It’s a trap, for three reasons. Tests: every test shares the same lot, so one test’s parked cars leak into the next. Our concurrency test in step 10 needs a fresh lot with exactly 50 spots; a Singleton makes that painful. Hidden connections: any class can grab the lot from anywhere, so you can’t tell from a class’s constructor what it depends on. Growth: the moment the company runs a second mall, “only one” is false and the Singleton has to be torn out everywhere.
The better answer: create one ParkingLot at startup and pass it to the gates that need it. There’s still only one, but nothing is forced to know that. If the interviewer pushes for a Singleton, say this, and offer it as a choice rather than refusing. Discussing the trade-off is what earns the credit.
Patterns we deliberately didn’t use
Saying where you didn’t use a pattern is as convincing as saying where you did.
- State, for spots (free, occupied, under repair). Right now a spot has two states, and “is the occupant empty?” handles them. If spots gain several states with different rules, State becomes worth it.
- Factory, for vehicles. There’s nothing to choose between: a vehicle is a record with a type.
- Builder, for the lot. The constructor takes five arguments, which is fine. With ten optional settings, a builder would earn its place.
Step 8Walk the flows
Now trace the two main flows, one call at a time, through the diagram. This is the most underrated step in LLD interviews. It’s where you find missing methods, wrong owners and race conditions, while changing them is still just redrawing a box.
A car arrives
What happens at each step, and why:
- The gate hands the vehicle to the lot. The gate knows nothing about floors or spots.
- The lot refuses a plate that’s already inside (FR7). A car can’t be in two places at once, so this catches a misread or cloned number plate early.
- The lot asks its strategy for a spot. The lot doesn’t decide; the policy does.
- Best fit: the strategy tries the smallest size that fits, on every floor, before trying a bigger size. A car looks for a medium spot on every floor before taking a large one, which keeps large spots for trucks. Our test run shows exactly this: with floor 0’s medium spot taken, the second car went to floor 1’s medium spot, not floor 0’s large one. The truck that came next still found its large spot.
- The floor asks a spot to take the vehicle. This is the moment that must be atomic: if two gates reach the same spot together, only one may win.
- The floor lowers its free count and tells its listeners, so boards change without the floor knowing they exist.
- The strategy returns the floor and the spot, as a
SpotAssignment. - The lot creates the ticket, remembers it in both maps, and returns it. The gate prints it and lifts the barrier.
A car leaves
- The gate hands over the ticket id and how the driver wants to pay.
- The lot claims the ticket first, by removing it from the active map in one atomic step. If the same ticket is scanned at two exits at once, only one removal succeeds, so only one exit can charge. Our first draft of this code got it wrong: it charged first and removed the ticket after, so two exits could both charge. Walking this flow is what exposed it.
- Pricing works out the fee. A car that stayed 75 minutes in a medium spot pays for 2 hours: ₹80.
- Payments charges the driver. If the charge fails, the lot puts the ticket back, so the driver can try another way to pay. Nothing is lost.
- The floor frees the spot and raises its count, and boards update.
- The gate gets a receipt and lifts the barrier.
What walking the flows taught us
Walking these two flows changed the design in three places. That’s normal, and it’s exactly why you do it before coding:
| Found while walking | Change |
|---|---|
| To free a spot, the exit must update that floor’s count, but a spot doesn’t know its floor | The strategy returns a SpotAssignment (floor + spot), and the ticket stores the floor |
| Charging before claiming the ticket lets two exits both charge | Claim the ticket first, give it back if payment fails |
| A failed payment shouldn’t lose the ticket | Put it back in the active map |
The life of a spot and a ticket
It also helps to write down the states each object moves through, and what moves it. If a move isn’t listed, it must not be possible.
| Spot is | Moves to | When |
|---|---|---|
| Free | Occupied | tryOccupy succeeds at entry |
| Occupied | Free | release at exit, after payment |
| Ticket is | Moves to | When |
|---|---|---|
| (doesn’t exist) | Active | park succeeds |
| Active | Being paid | unpark removes it from the map |
| Being paid | Closed | payment succeeds; spot freed |
| Being paid | Active | payment fails; ticket put back |
| Closed | — | any further use is refused |
When things go wrong
| Situation | What happens | Where it’s handled |
|---|---|---|
| No spot big enough | LotFullException; the gate shows “Full for cars” | ParkingLot.park |
| Plate already inside | Refused | ParkingLot.park |
| Payment fails | Ticket put back; driver tries again | ParkingLot.unpark |
| Ticket already used, or fake | Refused | ParkingLot.unpark |
| Two cars race for the last spot | One wins; the other tries the next spot | ParkingSpot.tryOccupy |
| One ticket at two exits | Only one exit succeeds | ParkingLot.unpark |
Step 9Write the code, bottom-up
Now we switch direction and code from the smallest piece that must be correct, upward. Each class below only uses classes written before it, so each one can be finished and tested before moving on.
All of this is complete Java. Put together, it compiles and runs, and every output shown on this page comes from actually running it. You won’t write every line in a real interview, but you should be able to write any of it when asked.
9.1 The vocabulary
The enums and the vehicle record from step 2, plus the payment mode:
enum SpotSize { SMALL, MEDIUM, LARGE } 1
enum VehicleType {
BIKE(SpotSize.SMALL), CAR(SpotSize.MEDIUM), TRUCK(SpotSize.LARGE);
private final SpotSize needs;
VehicleType(SpotSize needs) { this.needs = needs; }
SpotSize needs() { return needs; }
boolean fits(SpotSize spot) { return spot.compareTo(needs) >= 0; }
}
record Vehicle(String plate, VehicleType type) { }
enum PaymentMode { CASH, CARD, UPI }- 1order matters: smallest to largest
compareTo on an enum compares positions in the list, which is why the order of SMALL, MEDIUM, LARGE matters. A TRUCK needs LARGE, and only LARGE is at least as big.
9.2 ParkingSpot
class ParkingSpot {
private final String id;
private final SpotSize size;
private final AtomicReference occupant = new AtomicReference<>(); 1
ParkingSpot(String id, SpotSize size) { this.id = id; this.size = size; }
boolean tryOccupy(Vehicle v) { return occupant.compareAndSet(null, v); } 2
void release() { occupant.set(null); }
boolean isFree() { return occupant.get() == null; }
SpotSize size() { return size; }
String id() { return id; }
} - 1null means free
- 2only succeeds if still free
An AtomicReference is a box holding one value that several threads can safely read and change. Its compareAndSet(null, v) means “put this vehicle in, but only if the box is still empty”, and it does the check and the change as one indivisible step. Two gates can call it at the very same instant, and exactly one gets true. Chapter 7 explains how this works underneath.
id and size are final: a spot never changes size. Only the occupant changes, and it’s the only field that needs protecting.
9.3 ParkingFloor
interface FloorListener { void freeSpotsChanged(int level, SpotSize size, int free); }
class ParkingFloor {
private final int level;
private final Map> bySize = new EnumMap<>(SpotSize.class);
private final Map free = new EnumMap<>(SpotSize.class);
private final List listeners = new CopyOnWriteArrayList<>(); 1
ParkingFloor(int level, List spots) {
this.level = level;
for (SpotSize s : SpotSize.values()) {
bySize.put(s, new ArrayList<>());
free.put(s, new AtomicInteger());
}
for (ParkingSpot sp : spots) {
bySize.get(sp.size()).add(sp);
free.get(sp.size()).incrementAndGet();
}
}
Optional claim(Vehicle v, SpotSize size) {
if (free.get(size).get() == 0) return Optional.empty(); 2
for (ParkingSpot sp : bySize.get(size)) {
if (sp.isFree() && sp.tryOccupy(v)) { 3
changed(size, free.get(size).decrementAndGet());
return Optional.of(sp);
}
}
return Optional.empty();
}
void release(ParkingSpot sp) {
sp.release();
changed(sp.size(), free.get(sp.size()).incrementAndGet());
}
int freeCount(SpotSize s) { return free.get(s).get(); }
void subscribe(FloorListener l) { listeners.add(l); }
private void changed(SpotSize s, int n) {
for (FloorListener l : listeners) l.freeSpotsChanged(level, s, n);
}
} - 1safe to loop over while others subscribe
- 2quick skip when this size is full
- 3isFree() is a cheap filter; tryOccupy() decides
A few things worth pointing at:
- *
EnumMap* is a map whose keys are enum values. It’s faster and smaller than aHashMapfor this, and it keeps keys in enum order. - *
Optional* is a box that either holds a spot or is empty. It makes “no spot found” impossible to ignore, unlike returningnull. - Note (3):
isFree()alone would be the classic bug. Here it’s only a cheap way to skip spots that are obviously taken. The real decision istryOccupy(), which can still say no if another gate got there in between. - The free counts are
AtomicIntegers, so two gates changing the same count at once never lose an update.
9.4 Ticket and SpotAssignment
record SpotAssignment(ParkingFloor floor, ParkingSpot spot) { }
record Ticket(String id, Vehicle vehicle, ParkingSpot spot,
ParkingFloor floor, Instant entry) { }Two lines. Records give us the constructor, the getters, equals and hashCode for free, and they can’t be changed after they’re made. A ticket that can’t change can be handed to any thread safely.
9.5 The allocation strategy
interface SpotAllocationStrategy {
Optional allocate(List floors, Vehicle v);
}
class BestFitLowestFloor implements SpotAllocationStrategy {
public Optional allocate(List floors, Vehicle v) {
for (SpotSize size : SpotSize.values()) { 1
if (!v.type().fits(size)) continue;
for (ParkingFloor f : floors) { 2
Optional sp = f.claim(v, size);
if (sp.isPresent()) return Optional.of(new SpotAssignment(f, sp.get()));
}
}
return Optional.empty();
}
} - 1smallest size first: best fit
- 2then lowest floor first
The order of the two loops is the policy. Size on the outside means “use up every medium spot in the building before touching a large one”. Swap the loops and you get “fill the lowest floor first, with whatever size”. Both are valid rules, which is exactly why this is a strategy and not hard-coded in the lot.
9.6 Pricing
interface PricingStrategy { long fee(Ticket t, Instant exit); }
class HourlyPricing implements PricingStrategy {
private final Map perHour; // paise per hour
private final Duration grace;
HourlyPricing(Map perHour, Duration grace) {
this.perHour = Map.copyOf(perHour);
this.grace = grace;
}
public long fee(Ticket t, Instant exit) {
Duration stay = Duration.between(t.entry(), exit);
if (stay.compareTo(grace) <= 0) return 0; 1
long hours = (stay.toMinutes() + 59) / 60; 2
return hours * perHour.get(t.spot().size());
}
} - 1free for the first few minutes
- 2round up to whole hours
(minutes + 59) / 60 is a common trick for rounding up with whole numbers: 61 minutes becomes 120 / 60 = 2 hours, 60 minutes stays 1. Notice the fee depends on the spot’s size, not the vehicle’s: a bike that parked in a large spot pays the large rate. That’s a business rule worth checking with the interviewer.
Here’s what it charges, with ₹20, ₹40 and ₹80 per hour and 10 free minutes. Working through a table like this in the interview catches mistakes at the edges:
| Stay | Spot | Hours charged | Fee |
|---|---|---|---|
| 5 min | any | — | ₹0 |
| exactly 10 min | any | — | ₹0 |
| 11 min | medium | 1 | ₹40 |
| 60 min | medium | 1 | ₹40 |
| 61 min | medium | 2 | ₹80 |
| 75 min | medium | 2 | ₹80 |
| 3 h 20 min | large | 4 | ₹320 |
9.7 Payments
interface PaymentProcessor { boolean charge(long amountPaise, PaymentMode mode); }
record Receipt(String ticketId, String plate, Duration stay,
long amountPaise, PaymentMode mode) { }That’s all the lot needs. A real implementation would call a payment provider, and would probably pick a different provider per mode. Because PaymentProcessor has a single method, a test can supply one in a single line: (amount, mode) -> true.
9.8 ParkingLot
Now the front door, which ties everything together:
class LotFullException extends RuntimeException {
LotFullException(VehicleType t) { super("No free spot for a " + t); }
}
class ParkingLot {
private final List floors;
private final SpotAllocationStrategy allocation;
private final PricingStrategy pricing;
private final PaymentProcessor payments;
private final Clock clock;
private final Map activeByTicket = new ConcurrentHashMap<>();
private final Map activeByPlate = new ConcurrentHashMap<>();
ParkingLot(List floors, SpotAllocationStrategy allocation,
PricingStrategy pricing, PaymentProcessor payments, Clock clock) { 1
this.floors = List.copyOf(floors); this.allocation = allocation;
this.pricing = pricing; this.payments = payments; this.clock = clock;
}
Ticket park(Vehicle v) {
if (activeByPlate.containsKey(v.plate()))
throw new IllegalStateException(v.plate() + " is already parked");
SpotAssignment a = allocation.allocate(floors, v)
.orElseThrow(() -> new LotFullException(v.type()));
String id = UUID.randomUUID().toString();
Ticket t = new Ticket(id, v, a.spot(), a.floor(), clock.instant());
if (activeByPlate.putIfAbsent(v.plate(), t) != null) { 2
a.floor().release(a.spot());
throw new IllegalStateException(v.plate() + " is already parked");
}
activeByTicket.put(t.id(), t);
return t;
}
Optional activeTicketFor(String plate) {
return Optional.ofNullable(activeByPlate.get(plate));
}
Receipt unpark(String ticketId, PaymentMode mode) {
Ticket t = activeByTicket.remove(ticketId); 3
if (t == null)
throw new IllegalArgumentException("Unknown or already used ticket");
Instant exit = clock.instant();
long fee = pricing.fee(t, exit);
if (fee > 0 && !payments.charge(fee, mode)) {
activeByTicket.put(ticketId, t); 4
throw new IllegalStateException("Payment failed, please try again");
}
activeByPlate.remove(t.vehicle().plate(), t);
t.floor().release(t.spot());
Duration stay = Duration.between(t.entry(), exit);
return new Receipt(t.id(), t.vehicle().plate(), stay, fee, mode);
}
} - 1everything handed in: easy to test and to change
- 2the same plate at two gates at once
- 3claim the ticket first: only one exit can get it
- 4payment failed: give the ticket back
Read park top to bottom and it’s the entry flow from step 8, line for line. Read unpark and it’s the exit flow. That’s what a good facade looks like: short methods that coordinate, with the real work done elsewhere.
ConcurrentHashMap is a map that many threads can use at once. Its single-step methods, putIfAbsent (“add only if this key isn’t there”) and remove (“take it out and give it to me”), are what make notes (2) and (3) safe. Step 10 goes through why.
The ticket id is a random UUID, so it can’t be guessed. With a simple counter, someone could print ticket number 1042 at home.
9.9 Gates and the display board
class EntryGate {
private final String id; private final ParkingLot lot;
EntryGate(String id, ParkingLot lot) { this.id = id; this.lot = lot; }
Ticket arrived(Vehicle v) { return lot.park(v); }
}
class ExitGate {
private final String id; private final ParkingLot lot;
ExitGate(String id, ParkingLot lot) { this.id = id; this.lot = lot; }
Receipt leaving(String ticketId, PaymentMode mode) {
return lot.unpark(ticketId, mode);
}
}
class DisplayBoard implements FloorListener {
private final Map shown = new ConcurrentHashMap<>();
public void freeSpotsChanged(int level, SpotSize size, int free) {
shown.put(size, free);
System.out.println(" [board F" + level + "] " + size + " free: " + free);
}
} The gates hold an id they don’t use yet. It’s there for the receipt, logs and later rules (“gate 3 is for trucks only”). That’s one of the few places where a small “just in case” is reasonable: an id costs nothing and every real gate has one.
9.10 Putting it together
Here’s the setup: two floors, a test clock we can move forward, a board on floor 1, and a payment processor that always says yes.
ParkingFloor f0 = new ParkingFloor(0, spots(0, 1, 1, 1)); 1
ParkingFloor f1 = new ParkingFloor(1, spots(1, 0, 1, 1));
Map rates = Map.of(
SpotSize.SMALL, 2000L, SpotSize.MEDIUM, 4000L, SpotSize.LARGE, 8000L); 2
PricingStrategy hourly = new HourlyPricing(rates, Duration.ofMinutes(10));
ParkingLot lot = new ParkingLot(List.of(f0, f1), new BestFitLowestFloor(), hourly,
(amount, mode) -> true, clock);
EntryGate in = new EntryGate("E1", lot);
ExitGate out = new ExitGate("X1", lot);
f1.subscribe(new DisplayBoard());
Ticket bike = in.arrived(new Vehicle("KA01B", VehicleType.BIKE));
Ticket car1 = in.arrived(new Vehicle("KA02C", VehicleType.CAR));
Ticket car2 = in.arrived(new Vehicle("KA03C", VehicleType.CAR));
Ticket truck = in.arrived(new Vehicle("KA04T", VehicleType.TRUCK));
in.arrived(new Vehicle("KA02C", VehicleType.CAR)); 3
clock.advance(Duration.ofMinutes(5)); out.leaving(bike.id(), PaymentMode.UPI);
clock.advance(Duration.ofMinutes(70)); out.leaving(car1.id(), PaymentMode.CARD);
out.leaving(car1.id(), PaymentMode.CARD); 4 - 1small, medium, large: 1 each
- 2paise per hour
- 3same plate again
- 4the same ticket again
clock is a test clock: a Clock whose time only moves when we call advance. spots(level, small, medium, large) is a small helper that makes spots with ids like F0-M0. The prints are left out above; here’s the output from the real run:
[board F1] MEDIUM free: 0
bike -> F0-S0, car1 -> F0-M0, car2 -> F1-M0, truck -> F0-L0
same plate again: KA02C is already parked
bike leaves after 5 min, fee = 0
car1 after 75 min, fee = 8000
reused ticket: Unknown or already used ticketCheck each line against what we designed. The bike took the small spot. The first car took floor 0’s medium spot. The second car went up a floor to a medium spot instead of taking floor 0’s large one: best fit. So the truck still found its large spot. Floor 1’s board updated when its medium spot went. The duplicate plate and the reused ticket were both refused (FR7). Five minutes was free; 75 minutes cost two hours of medium parking, 8000 paise, which is ₹80.
And a failed payment, with a processor that declines the first charge:
card declined: Payment failed, please try again
retry with UPI, fee = 4000The ticket survived the failure, and the driver paid the second time.
Step 10Make it safe when gates run at once
Interviewer: Several gates work at the same time. Where can this go wrong?
You: Anywhere two threads share something that changes. Let me list what’s shared.
That’s the right first move. Don’t say “add synchronized”. List the shared, changing state, then protect each piece with the smallest tool that works:
| Shared state | Who changes it | Danger | Protection |
|---|---|---|---|
| a spot’s occupant | every entry and exit | two cars in one spot | compareAndSet on the spot |
| a floor’s free counts | every entry and exit | lost updates, wrong board | AtomicInteger |
| active tickets by id | entry adds, exit removes | one ticket paid twice | ConcurrentHashMap.remove, one step |
| active tickets by plate | entry adds, exit removes | same car parked twice | ConcurrentHashMap.putIfAbsent, one step |
| a floor’s listener list | subscribe, notify | crash while looping | CopyOnWriteArrayList |
The race at the spot
Here is the classic bug, slowed down. Two gates each have a car, and one spot is left:
| Time | Gate A | Gate B | Spot |
|---|---|---|---|
| 1 | is the spot free? yes | free | |
| 2 | is the spot free? yes | free | |
| 3 | take it for car A | car A | |
| 4 | take it for car B | car B (car A’s ticket now points at someone else’s car) |
Both gates checked, both saw “free”, both took it. Two tickets, one spot, and a very confused driver. The problem isn’t the check or the take; it’s the gap between them.
With compareAndSet, checking and taking are one step that can’t be split:
| Time | Gate A | Gate B | Spot |
|---|---|---|---|
| 1 | take it, only if free → success | car A | |
| 2 | take it, only if free → fails | car A | |
| 3 | try the next spot | car A |
Try both versions:
The race at the exit
The same shape of bug hides in the exit flow. If unpark checks “is this ticket active?”, then charges, then removes the ticket, two exits scanning the same ticket both see it active, and both charge. Using remove() as the first step means the map hands the ticket to exactly one caller; the other gets null and is refused before any money moves.
Why not one big lock?
Interviewer: Why not just put one lock around park and unpark?
You: It would be correct, and for a small lot it would be fine. But then every gate waits for every other gate, even when there are 300 free spots and they want different ones. With per-spot claiming, gates only compete when they want the very same spot, which is rare.
You: The other cost is risk. With a big lock, a slow payment provider holds the lock, and the whole lot’s entry gates stall behind one driver’s card.
That last point is the strongest one: never hold a lock while calling an outside system. Our unpark calls the payment provider without holding anything; the ticket is already safely claimed.
Prove it
Claims about concurrency are cheap. Tests are convincing. This one sends a hundred cars at fifty spots through eight gates, all released at the same instant by a latch (a gate that holds threads until it opens):
ExecutorService gates = Executors.newFixedThreadPool(8);
CountDownLatch go = new CountDownLatch(1);
for (int i = 0; i < 100; i++) {
final int n = i;
gates.submit(() -> {
go.await(); 1
try { issued.add(lot.park(new Vehicle("CAR" + n, VehicleType.CAR))); }
catch (LotFullException e) { turnedAway.incrementAndGet(); }
return null;
});
}
go.countDown(); 2- 1everyone waits here…
- 2…then all go at once
A second test scans one ticket at two exits at the same moment. The results, the same on every run:
same ticket at two exits: successful exits=1
race: tickets=50, distinct spots=50, turned away=50, free left=0Fifty tickets, fifty different spots, fifty cars turned away, nothing left over, and one successful exit. If the spot used “check, then take”, the distinct-spots number would sometimes come out below the tickets number.
One honest detail worth mentioning: if the same plate arrives at two gates at once, both may briefly claim a spot before one is refused by putIfAbsent. The code gives that spot straight back, so it’s correct, but a display board might flicker for a moment. Checking the plate first, as park does, makes that rare. Pointing out a small imperfection like this, and why it’s acceptable, impresses interviewers more than claiming perfection.
Step 11Follow-ups, extended without editing
This is where the design pays off. The interviewer adds requirements, and a good design answers each one by adding classes or wrapping existing ones, not editing tested code. That’s the open/closed principle from chapter 2: open for extension, closed for modification.
Electric charging spots
Interviewer: The mall adds charging spots. Only electric cars that want to charge may use them.
Bad solutionif (vehicle.isElectric()) scattered through the code
The strategy, the floor and pricing all grow special cases, and the next feature adds more.
Good solutionSpots get features, vehicles get needs
Give each spot a set of features and each vehicle type a set of requirements. “Fits” checks size, then features:
enum Feature { CHARGER, ACCESSIBLE }
enum VehicleType {
BIKE(SpotSize.SMALL, Set.of()),
CAR(SpotSize.MEDIUM, Set.of()),
TRUCK(SpotSize.LARGE, Set.of()),
ELECTRIC_CAR(SpotSize.MEDIUM, Set.of(Feature.CHARGER));
private final SpotSize needs;
private final Set requires;
VehicleType(SpotSize needs, Set requires) {
this.needs = needs;
this.requires = requires;
}
boolean fits(ParkingSpot s) {
boolean bigEnough = s.size().compareTo(needs) >= 0;
boolean hasNeeds = s.features().containsAll(requires); 1
boolean isCharger = s.features().contains(Feature.CHARGER);
boolean wantsCharge = requires.contains(Feature.CHARGER);
return bigEnough && hasNeeds && (wantsCharge || !isCharger); 2
}
} - 1has what the vehicle needs
- 2chargers kept for EVs
Running it: a car fits a plain spot but not a charger spot; an electric car that wants to charge fits only a charger spot. (An electric car that doesn’t want to charge just arrives as a CAR.) No new if anywhere outside this one method. The same pattern handles accessible spots.
Great solutionAdd charging to pricing by wrapping, not editing
Charging is billed by energy used. Don’t touch HourlyPricing. Wrap it:
interface ChargerMeter { long wattHours(String ticketId); }
class ChargingSurcharge implements PricingStrategy {
private final PricingStrategy inner;
private final ChargerMeter meter;
private final long paisePerKwh;
ChargingSurcharge(PricingStrategy inner, ChargerMeter meter, long paisePerKwh) {
this.inner = inner; this.meter = meter; this.paisePerKwh = paisePerKwh;
}
public long fee(Ticket t, Instant exit) {
return inner.fee(t, exit) + meter.wattHours(t.id()) * paisePerKwh / 1000; 1
}
}- 1parking + energy
ChargingSurcharge is a PricingStrategy and holds one. It asks the inner one for the parking fee and adds energy on top. That’s the Decorator pattern from chapter 5. Two hours in a medium spot plus 18.5 kWh at ₹12 per kWh: ₹80 + ₹222.
ev 2h + 18.5 kWh = 30200Weekend rates and a daily cap
Interviewer: Weekends cost 1.5 times as much. And nobody should pay more than ₹300 a day.
Bad solutionAdd if (isWeekend) inside HourlyPricing
Every new rule makes one class bigger and riskier to change. Six months later it has nine flags.
Good solutionA new strategy class per rule
WeekendPricing implements PricingStrategy, picked by the day of the week. Each rule is separate and testable, but “weekend and capped” now needs a third class, and combinations multiply.
Great solutionCompose small rules
Each rule is a small decorator, and they stack:
class WeekendMultiplier implements PricingStrategy {
private final PricingStrategy inner;
private final long num, den;
WeekendMultiplier(PricingStrategy inner, long num, long den) {
this.inner = inner; this.num = num; this.den = den;
}
public long fee(Ticket t, Instant exit) {
long base = inner.fee(t, exit);
DayOfWeek d = t.entry().atZone(ZoneId.of("Asia/Kolkata")).getDayOfWeek(); 1
boolean weekend = d == DayOfWeek.SATURDAY || d == DayOfWeek.SUNDAY;
return weekend ? base * num / den : base; 2
}
}
class DailyCap implements PricingStrategy {
private final PricingStrategy inner;
private final long capPerDay;
DailyCap(PricingStrategy inner, long capPerDay) {
this.inner = inner; this.capPerDay = capPerDay;
}
public long fee(Ticket t, Instant exit) {
long minutes = Duration.between(t.entry(), exit).toMinutes();
long days = Math.max(1, (minutes + 1439) / 1440); 3
return Math.min(inner.fee(t, exit), days * capPerDay);
}
}
PricingStrategy pricing = new DailyCap(new WeekendMultiplier(hourly, 3, 2), 30000);- 1the day you arrived decides
- 23/2 instead of 1.5: whole numbers
- 3each started day has its own cap
The lot still sees one PricingStrategy and doesn’t change at all. The results:
weekday 3h = 12000
weekend 3h = 18000
weekday 20h (capped) = 30000Two questions to raise with the interviewer, because the code had to answer them: does the entry day or the exit day decide the weekend rate? (We chose entry.) And does the cap apply before or after the weekend multiplier? (We cap last, so the cap is a promise to the driver.) Raising these shows you’re thinking about the business, not just the code.
A lost ticket
Interviewer: A driver has lost their ticket. How do they get out?
Bad solutionLet them leave without paying
Now everyone “loses” their ticket.
Good solutionCharge a fixed lost-ticket fee
Simple and common, but someone who parked for ten minutes pays a day’s fee.
Great solutionFind the ticket by number plate
We already keep activeByPlate to stop double parking (FR7). The exit gate can look the ticket up by plate, read by a camera or typed by an attendant, and charge the real fee:
Receipt leavingWithoutTicket(String plate, PaymentMode mode) {
Ticket t = lot.activeTicketFor(plate)
.orElseThrow(() -> new IllegalArgumentException(
"No car with plate " + plate + " is inside. Call an attendant."));
return lot.unpark(t.id(), mode);
}KA03C lost its ticket, fee = 8000A map added for one requirement quietly made another one easy. Point that out; it’s a sign the design’s pieces are in the right places.
A different way to choose spots
Interviewer: Floor 0 gets crowded while floor 3 sits empty. Can we spread cars out?
A new strategy, and nothing else changes:
class EmptiestFloorFirst implements SpotAllocationStrategy {
public Optional allocate(List floors, Vehicle v) {
for (SpotSize size : SpotSize.values()) {
if (!v.type().fits(size)) continue;
List order = new ArrayList<>(floors);
order.sort(Comparator.comparingInt( 1
(ParkingFloor f) -> f.freeCount(size)).reversed());
for (ParkingFloor f : order) {
Optional sp = f.claim(v, size);
if (sp.isPresent()) return Optional.of(new SpotAssignment(f, sp.get()));
}
}
return Optional.empty();
}
} - 1most free first
With two floors of three medium spots each, four cars now alternate:
emptiest floor first: F0-M0 F1-M0 F0-M1 F1-M1This only works because the floor already keeps free counts. Counting from scratch on every arrival would have been slow.
Reservations
Interviewer: Customers can book a spot in advance from an app.
This one changes the model, so talk it through rather than coding it. A spot gets a third state, reserved, alongside free and occupied. A reservation is a new class with a time window and a hold that expires (say, 15 minutes after the booked time) so no-shows don’t block spots forever. The allocation strategy skips reserved spots unless the arriving plate matches the booking.
Now a spot has three states with different rules, and “is the occupant empty?” isn’t enough. This is the moment the State pattern, which we deliberately skipped in step 7, starts to earn its place. Saying “I didn’t add it before because we didn’t need it; now we do” is exactly the judgement interviewers want.
Surviving a restart
Interviewer: If the server restarts, all active tickets are lost. Fix that.
Put storage behind an interface, so the lot doesn’t care where tickets live:
interface TicketRepository {
void save(Ticket t);
Optional removeActive(String ticketId); 1
Optional findActiveByPlate(String plate);
} - 1must be atomic in the database too
The in-memory maps become one implementation; a database becomes another. The atomic “remove” now needs a database equivalent, such as an update that only succeeds if the ticket’s status is still active, and checks how many rows changed. The rule from step 10 doesn’t go away when the data moves; it moves with it.
Mistakes people make on this one
- Drawing classes before listing requirements, then discovering halfway that the design can’t answer “who picks the spot?”.
- A class per vehicle type, plus
instanceofchains, when vehicles differ only in size. - *Adding
Driver,Customer,Attendant* with no data or rules of their own. - *Putting everything in
ParkingLot:* allocation, pricing, payments and counting, so every change touches one huge class. - *Making
ParkingLota Singleton* and then struggling to test it. - *Claiming a spot with “check
isFree(), then take it”,* which lets two cars take the same spot. - Charging at the exit before claiming the ticket, so one ticket can be paid twice.
- Holding a lock while calling the payment provider, so one slow card stalls every gate.
- *Using
doublefor money.* - *Answering follow-ups with new
ifstatements* in existing classes.
How this answer gets scored
| Moment | What earns credit | What costs you |
|---|---|---|
| The first five minutes | Requirements and non-functional needs, said out loud and numbered | Jumping straight to classes |
| Finding classes | Nouns filtered by “own data, own rules”, with reasons; traps named and dropped | A class for every noun |
| Vehicles | One enum with a size, and why inheritance is wrong here | Car extends Vehicle with instanceof checks |
| Responsibilities | Each method on the class that has the information | A ParkingLot that does everything |
| What varies | Strategy and Observer, because the requirements asked | Patterns added for decoration; Singleton by reflex |
| Flows | Walking entry and exit call by call, and fixing what that reveals | Coding first and patching later |
| Code | Small classes, exact money, a clock passed in, runs as described | Pseudo-code that hides the hard parts |
| Concurrency | Listing shared state; atomic claims on spots and tickets; why not one big lock | “Add synchronized everywhere” |
| Follow-ups | Adding and wrapping classes, not editing them | New if statements in old classes |
The whole design on one page
If you need to rebuild this from memory the night before an interview, here it is in six lines:
- Structure:
ParkingLot→ floors → spots, withSpotSizeand aVehicleTypeenum that knows the size it needs. - Front door:
ParkingLot.parkandunpark, called by thin gates. The lot only coordinates. - What varies: allocation, pricing and payments are strategies; display boards are observers of floors.
- Entry: best fit (smallest size, then lowest floor), spot taken with one
compareAndSet, ticket remembered by id and by plate. - Exit: claim the ticket first with one
remove, price it, charge it, give it back if payment fails, then free the spot. - Follow-ups: features on spots, decorators on pricing, a new strategy class, a repository for storage. Add, don’t edit.
Stay in the loop
Get new problems and walkthroughs by email
One short email when something new is published: a deep-dive walkthrough, an LLD problem, or a real outage retold. No spam, unsubscribe any time.