System Design Road

Easy LLD interview problem

Design a parking lot

“Design a parking lot” looks easy, and that’s exactly why it’s asked. There’s no clever algorithm to hide behind. The interviewer is watching how you turn a vague sentence into a set of small, well-behaved objects. This walkthrough builds the whole design from nothing, one decision at a time, and explains why each decision went the way it did.

42 min read5 diagrams15 sections

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:

StepWhat you doWhat comes out of it
1. Understand the problemAsk questions, write down requirementsA short list of what the system must do, and how well
2. Break it into entitiesNouns → classes, verbs → methods, find what changesA list of classes, each with one clear job
3. Choose a directionTop-down for designing, bottom-up for codingThe order you’ll work in
4. Design each classDecide what each class knows and doesA “card” per class
5. Connect the classesPick the right kind of relationshipWho owns whom, who uses whom
6. Draw the diagramBuild it up in three versionsThe complete class diagram
7. Name the patternsSpot where the requirements asked for oneStrategy, Observer, Facade, and why not Singleton
8. Walk the flowsTrace entry and exit call by callGaps found while they’re cheap to fix
9. Write the codeBottom-up, one class at a timeCode that compiles and runs
10. Make it safeFind shared state, fix the racesTwo atomic claims and a test that proves them
11. Extend itAnswer follow-upsNew 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

Interviewer: Design a parking lot system.

You

You: Sure. Let me ask a few questions first. Is this one building with several floors, or several separate lots?

Interviewer

Interviewer: One building, several floors, each with many spots.

You

You: What kinds of vehicles do we handle, and are the spots different sizes?

Interviewer

Interviewer: Bikes, cars and trucks. Spots are small, medium and large.

You

You: Can a vehicle park in a spot bigger than it needs?

Interviewer

Interviewer: Yes. A bike can use any spot, a car can use medium or large, a truck needs large.

You

You: Does the system assign the spot, or does the driver choose?

Interviewer

Interviewer: The system assigns it, and prints it on the ticket.

You

You: How do people get in and out?

Interviewer

Interviewer: Through entry gates and exit gates. There are several of each, all working at the same time.

You

You: How is the fee worked out?

Interviewer

Interviewer: By the hour, with a different rate for each spot size. The first ten minutes are free.

You

You: Will those pricing rules change?

Interviewer

Interviewer: Definitely. The mall changes them every few months.

You

You: How do people pay?

Interviewer

Interviewer: Cash, card or UPI.

You

You: Should anything show how many spots are free?

Interviewer

Interviewer: Yes, each floor has a display board at the ramp.

You

You: What about reservations, electric charging, lost tickets?

Interviewer

Interviewer: Not for now. We might come back to them.

That’s eleven questions in about four minutes. Each one changed something:

QuestionWhat 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 itemsSaying 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.

  1. FR1. The lot has several floors. Each floor has small, medium and large spots.
  2. FR2. A vehicle (bike, car or truck) can park in any free spot that’s big enough for it.
  3. FR3. At an entry gate, the system picks a spot and issues a ticket with the spot and the time.
  4. FR4. At an exit gate, the system works out the fee from the time and the spot size.
  5. FR5. The driver pays by cash, card or UPI. The spot is then freed.
  6. FR6. Each floor’s display board shows how many spots of each size are free.
  7. 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.

  1. NFR1: Easy to extend. New vehicle types, allocation rules and pricing rules should mean adding code, not editing working code.
  2. 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.
  3. 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.
  4. 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:

AssumptionWhy it’s reasonable
The spot is assigned automatically at the gateThe interviewer said so
Best fit: use the smallest size that fits, then the lowest floorKeeps 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 chargedMatches how most Indian malls charge
Rates: small ₹20/hour, medium ₹40/hour, large ₹80/hourExample numbers so we can test
Number-plate reading is done by the gate hardwareIt’s outside our software; we receive a plate as text
Everything lives in memory for nowSaving 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:

  1. Does it have its own data that changes over time, or that identifies it?
  2. Does it have its own rules, things it must check or protect?
  3. 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.

NounOwn data?Own rules?Changes alone?Becomes
Lotfloors, active ticketsno car parked twice—ParkingLot class
Floorits spots, free countsfinds a free spot of a size—ParkingFloor class
Spotid, size, who’s in itonly one vehicle at a time—ParkingSpot class
Size———SpotSize enum: three fixed values
Vehicleplate, type——Vehicle record (plain data)
Bike, car, truck—only “which size do I need”—values of a VehicleType enum (see the trap below)
Ticketid, vehicle, spot, entry time——Ticket record
Fee, rate—how a fee is worked outyes, oftenPricingStrategy interface
Payment——yes (new providers)PaymentProcessor interface
Cash, card, UPI———PaymentMode enum
Entry gate, exit gategate id——thin EntryGate, ExitGate classes
Display boardnumbers 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:

Java
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;
}
  1. 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:

Java
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) { }
  1. 1listed smallest to largest, so sizes can be compared
  2. 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 caseWho starts itVerbs inside it
UC1: ParkEntry gatecheck the plate isn’t inside, pick a spot, take the spot, issue a ticket, update the board
UC2: LeaveExit gatecheck the ticket, work out the fee, take payment, free the spot, update the board
UC3: See free spotsDisplay boardcount 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.

JobInformation neededWho has itSo the owner is
Is this spot free? Take it. Free it.who’s in the spotthe spotParkingSpot
Find a free spot of a size on this floorthe floor’s list of spotsthe floorParkingFloor
Count free spots per sizeevery take and free on the floorthe floorParkingFloor
Does this vehicle fit this size?what size the vehicle needsthe vehicle typeVehicleType
Which floor and size to try first?all floors, plus a policynobody, yet: it’s a rule that will changeSpotAllocationStrategy
How much does this stay cost?entry time, exit time, spot size, a policythe ticket has the facts; the policy will changePricingStrategy
Charge the moneythe bank, the UPI provideroutside systemsPaymentProcessor
Is this plate already parked? Is this ticket valid?all active ticketsonly the lot sees all of themParkingLot

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).

ClassResponsibilitiesCollaborators
ParkingSpottake a vehicle safely; free itself; report its sizeVehicle
ParkingFloorfind and take a free spot of a size; free spots; keep counts; tell listenersParkingSpot, FloorListener
SpotAllocationStrategychoose which floor and size to try, in what orderParkingFloor, VehicleType
PricingStrategywork out a fee for a ticket and an exit timeTicket
PaymentProcessorcharge an amount by a payment modeoutside systems
ParkingLotpark and unpark; reject duplicates; remember active ticketsall of the above
EntryGate, ExitGatepass requests from the hardware to the lotParkingLot
DisplayBoardshow 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 changeHint from the interviewerBecomes
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:

  1. The entry gate needs to call park(vehicle) and get a ticket back. So ParkingLot.park() exists.
  2. To park, the lot needs to choose a spot. So it needs a SpotAllocationStrategy with allocate(floors, vehicle).
  3. To allocate, the strategy needs floors that can hand over a free spot of a given size. So ParkingFloor.claim(vehicle, size) exists.
  4. 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:

Building the parking lot design: two directions
6
EntryGateneeds: park(vehicle)ParkingLotpark(), unpark()SpotAllocationwhich spot?ParkingFloorclaim(vehicle, size)ParkingSpottryOccupy(vehicle)SpotSize, VehicleTypefits(size)

Every class exists because the one above needed it. Nothing was built that nobody uses. The risk: the hardest detail, safe claiming, was designed last.

Direction
Top-downBottom-up
Starts fromWhat callers need: the public methodsThe smallest building blocks
StrengthNothing useless gets built. The interviewer sees your API early.Each piece is finished and testable before anything depends on it.
RiskThe 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 forDesigning: the class diagram and the flowsCoding: 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.

KnowsDoesDoesn’t do
its floorspark(vehicle) → Ticketchoose spots (asks the strategy)
an allocation strategyunpark(ticketId, mode) → Receiptwork out fees (asks pricing)
a pricing strategyactiveTicketFor(plate)talk to banks (asks payments)
a payment processorcount 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.

KnowsDoesDoesn’t do
its level numberclaim(vehicle, size) → spot or nothingdecide which size to try (the strategy does)
its spots, grouped by sizerelease(spot)know what a display board is
free count per sizefreeCount(size)
its listenerssubscribe(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.

KnowsDoesDoesn’t do
its id (like F1-M3)tryOccupy(vehicle) → true/falsedecide whether a vehicle fits (the vehicle type does)
its sizerelease()
who’s in it, or nobodyisFree()

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:

RelationshipMeaningIn plain wordsIn 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.

ParkingLot- floors: List+ park(v: Vehicle): Ticket+ unpark(ticketId, mode): ReceiptParkingFloor- level: int+ claim(v, size): OptionalParkingSpot- id: String- size: SpotSize+ tryOccupy(v: Vehicle): boolean+ release(): voidTicket- vehicle: Vehicle- spot: ParkingSpot- entry: Instant1..*1..*

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.

ParkingLot- allocation: SpotAllocationStrategy- pricing: PricingStrategy- payments: PaymentProcessor+ park(v: Vehicle): Ticket+ unpark(ticketId, mode): Receipt«interface»SpotAllocationStrategy+ allocate(floors, v): OptionalBestFitLowestFloor+ allocate(floors, v): Optional«interface»PricingStrategy+ fee(t: Ticket, exit: Instant): longHourlyPricing- perHour: Map- grace: Duration«interface»PaymentProcessor+ charge(amount: long, mode): boolean

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:

EntryGate- id: String+ arrived(v: Vehicle): TicketExitGate- id: String+ leaving(ticketId, mode): ReceiptTicket- id: String- vehicle: Vehicle- spot: ParkingSpot- floor: ParkingFloor- entry: InstantParkingLot- floors: List- allocation: SpotAllocationStrategy- pricing: PricingStrategy- payments: PaymentProcessor- activeByTicket: Map+ park(v: Vehicle): Ticket+ unpark(ticketId, mode): ReceiptParkingFloor- level: int- spots: Map>- free: Map+ claim(v, size): Optional+ release(spot: ParkingSpot): voidParkingSpot- id: String- size: SpotSize- occupant: AtomicReference+ tryOccupy(v: Vehicle): boolean+ release(): void«interface»SpotAllocationStrategy+ allocate(floors, v): Optional«interface»PricingStrategy+ fee(t: Ticket, exit: Instant): long«interface»PaymentProcessor+ charge(amount: long, mode): boolean«interface»FloorListener+ freeSpotsChanged(level, size, free): voidDisplayBoard+ freeSpotsChanged(level, size, free): void1..*1..*activenotifies

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.

RequirementWhere it lives
FR1: floors with three spot sizesParkingLot → ParkingFloor → ParkingSpot, SpotSize
FR2: park in any spot big enoughVehicleType.fits, used by BestFitLowestFloor
FR3: pick a spot, issue a ticket at entryEntryGate.arrived → ParkingLot.park
FR4: fee by time and sizePricingStrategy.fee, using the ticket’s entry time and spot size
FR5: pay by cash, card, UPI, then free the spotParkingLot.unpark → PaymentProcessor.charge → ParkingFloor.release
FR6: display boardsParkingFloor notifies FloorListener → DisplayBoard
FR7: no duplicate parking, no ticket reuseParkingLot‘s two maps
NFR1: easy to extendfour interfaces: allocation, pricing, payments, listeners
NFR2: correct under concurrencyParkingSpot.tryOccupy, atomic ticket removal (step 10)
NFR3: testableClock and PaymentProcessor passed in
NFR4: exact moneyfees 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.

PatternWhereThe problem it solvedWithout it
StrategySpotAllocationStrategy, PricingStrategy, PaymentProcessorRules that change often must be swappableif (mode == WEEKEND) branches growing inside the lot
ObserverFloorListener, DisplayBoardSeveral things react to spot changes, and the list will growThe floor calls boards directly and must change for each new screen
FacadeParkingLotGates need one simple entry point to a group of classesGates juggle floors, strategies and payments themselves
Value objectsVehicle, Ticket, Receipt, SpotAssignmentData that’s passed around should never change by surpriseSome 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

EntryGateParkingLotStrategyParkingFloorParkingSpot1 park(KA02C, CAR)2 plate already inside?3 allocate(floors, vehicle)4 claim(vehicle, MEDIUM)5 tryOccupy(vehicle)6 true7 free count −1, tell listeners8 spot F0-M09 SpotAssignment(F0, M0)10 create ticket, remember it11 ticket

What happens at each step, and why:

  1. The gate hands the vehicle to the lot. The gate knows nothing about floors or spots.
  2. 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.
  3. The lot asks its strategy for a spot. The lot doesn’t decide; the policy does.
  4. 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.
  5. 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.
  6. The floor lowers its free count and tells its listeners, so boards change without the floor knowing they exist.
  7. The strategy returns the floor and the spot, as a SpotAssignment.
  8. The lot creates the ticket, remembers it in both maps, and returns it. The gate prints it and lifts the barrier.

A car leaves

ExitGateParkingLotPricingPaymentsParkingFloor1 unpark(ticketId, UPI)2 remove ticket from active (claim it)3 fee(ticket, now)4 ₹805 charge(8000, UPI)6 success7 release(spot)8 free count +1, tell listeners9 receipt
  1. The gate hands over the ticket id and how the driver wants to pay.
  2. 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.
  3. Pricing works out the fee. A car that stayed 75 minutes in a medium spot pays for 2 hours: ₹80.
  4. 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.
  5. The floor frees the spot and raises its count, and boards update.
  6. 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 walkingChange
To free a spot, the exit must update that floor’s count, but a spot doesn’t know its floorThe strategy returns a SpotAssignment (floor + spot), and the ticket stores the floor
Charging before claiming the ticket lets two exits both chargeClaim the ticket first, give it back if payment fails
A failed payment shouldn’t lose the ticketPut 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 isMoves toWhen
FreeOccupiedtryOccupy succeeds at entry
OccupiedFreerelease at exit, after payment
Ticket isMoves toWhen
(doesn’t exist)Activepark succeeds
ActiveBeing paidunpark removes it from the map
Being paidClosedpayment succeeds; spot freed
Being paidActivepayment fails; ticket put back
Closed—any further use is refused

When things go wrong

SituationWhat happensWhere it’s handled
No spot big enoughLotFullException; the gate shows “Full for cars”ParkingLot.park
Plate already insideRefusedParkingLot.park
Payment failsTicket put back; driver tries againParkingLot.unpark
Ticket already used, or fakeRefusedParkingLot.unpark
Two cars race for the last spotOne wins; the other tries the next spotParkingSpot.tryOccupy
One ticket at two exitsOnly one exit succeedsParkingLot.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:

Java
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 }
  1. 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

Java
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; }
}
  1. 1null means free
  2. 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

Java
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);
    }
}
  1. 1safe to loop over while others subscribe
  2. 2quick skip when this size is full
  3. 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 a HashMap for 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 returning null.
  • 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 is tryOccupy(), 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

Java
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

Java
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();
    }
}
  1. 1smallest size first: best fit
  2. 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

Java
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());
    }
}
  1. 1free for the first few minutes
  2. 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:

StaySpotHours chargedFee
5 minany—₹0
exactly 10 minany—₹0
11 minmedium1₹40
60 minmedium1₹40
61 minmedium2₹80
75 minmedium2₹80
3 h 20 minlarge4₹320

9.7 Payments

Java
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:

Java
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);
    }
}
  1. 1everything handed in: easy to test and to change
  2. 2the same plate at two gates at once
  3. 3claim the ticket first: only one exit can get it
  4. 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

Java
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.

Java
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
  1. 1small, medium, large: 1 each
  2. 2paise per hour
  3. 3same plate again
  4. 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:

Text
  [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 ticket

Check 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:

Text
card declined: Payment failed, please try again
retry with UPI, fee = 4000

The ticket survived the failure, and the driver paid the second time.

Step 10Make it safe when gates run at once

Interviewer

Interviewer: Several gates work at the same time. Where can this go wrong?

You

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 stateWho changes itDangerProtection
a spot’s occupantevery entry and exittwo cars in one spotcompareAndSet on the spot
a floor’s free countsevery entry and exitlost updates, wrong boardAtomicInteger
active tickets by identry adds, exit removesone ticket paid twiceConcurrentHashMap.remove, one step
active tickets by plateentry adds, exit removessame car parked twiceConcurrentHashMap.putIfAbsent, one step
a floor’s listener listsubscribe, notifycrash while loopingCopyOnWriteArrayList

The race at the spot

Here is the classic bug, slowed down. Two gates each have a car, and one spot is left:

TimeGate AGate BSpot
1is the spot free? yesfree
2is the spot free? yesfree
3take it for car Acar A
4take it for car Bcar 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:

TimeGate AGate BSpot
1take it, only if free → successcar A
2take it, only if free → failscar A
3try the next spotcar A

Try both versions:

Two gates, one free spot
t3
Entry gate 1isFree()? yesEntry gate 2isFree()? yesSpot F0-M7taken by KA02C… then MH12XTickets2 tickets, same spot

Two tickets for one spot. One driver will find another car in their space. Under real traffic this happens every busy evening.

Taking a spot

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

Interviewer: Why not just put one lock around park and unpark?

You

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):

Java
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
  1. 1everyone waits here…
  2. 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:

Text
same ticket at two exits: successful exits=1
race: tickets=50, distinct spots=50, turned away=50, free left=0

Fifty 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

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:

Java
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
    }
}
  1. 1has what the vehicle needs
  2. 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:

Java
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
    }
}
  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.

Text
ev 2h + 18.5 kWh = 30200

Weekend rates and a daily cap

Interviewer

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:

Java
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);
  1. 1the day you arrived decides
  2. 23/2 instead of 1.5: whole numbers
  3. 3each started day has its own cap

The lot still sees one PricingStrategy and doesn’t change at all. The results:

Text
weekday 3h = 12000
weekend 3h = 18000
weekday 20h (capped) = 30000

Two 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

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:

Java
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);
}
Text
KA03C lost its ticket, fee = 8000

A 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

Interviewer: Floor 0 gets crowded while floor 3 sits empty. Can we spread cars out?

A new strategy, and nothing else changes:

Java
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();
    }
}
  1. 1most free first

With two floors of three medium spots each, four cars now alternate:

Text
emptiest floor first: F0-M0 F1-M0 F0-M1 F1-M1

This only works because the floor already keeps free counts. Counting from scratch on every arrival would have been slow.

Reservations

Interviewer

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

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:

Java
interface TicketRepository {
    void save(Ticket t);
    Optional removeActive(String ticketId); 1
    Optional findActiveByPlate(String plate);
}
  1. 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 instanceof chains, 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 ParkingLot a 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 double for money.*
  • *Answering follow-ups with new if statements* in existing classes.

How this answer gets scored

MomentWhat earns creditWhat costs you
The first five minutesRequirements and non-functional needs, said out loud and numberedJumping straight to classes
Finding classesNouns filtered by “own data, own rules”, with reasons; traps named and droppedA class for every noun
VehiclesOne enum with a size, and why inheritance is wrong hereCar extends Vehicle with instanceof checks
ResponsibilitiesEach method on the class that has the informationA ParkingLot that does everything
What variesStrategy and Observer, because the requirements askedPatterns added for decoration; Singleton by reflex
FlowsWalking entry and exit call by call, and fixing what that revealsCoding first and patching later
CodeSmall classes, exact money, a clock passed in, runs as describedPseudo-code that hides the hard parts
ConcurrencyListing shared state; atomic claims on spots and tickets; why not one big lock“Add synchronized everywhere”
Follow-upsAdding and wrapping classes, not editing themNew 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:

  1. Structure: ParkingLot → floors → spots, with SpotSize and a VehicleType enum that knows the size it needs.
  2. Front door: ParkingLot.park and unpark, called by thin gates. The lot only coordinates.
  3. What varies: allocation, pricing and payments are strategies; display boards are observers of floors.
  4. Entry: best fit (smallest size, then lowest floor), spot taken with one compareAndSet, ticket remembered by id and by plate.
  5. Exit: claim the ticket first with one remove, price it, charge it, give it back if payment fails, then free the spot.
  6. Follow-ups: features on spots, decorators on pricing, a new strategy class, a repository for storage. Add, don’t edit.