Parking lot
Part of the free Low-Level Design course on LogicWiz, in the chapter: Case studies, step by step. The class-diagram exercises run in the app.
Parking lot: finding the right size for a first draft
"You drew fifteen boxes in five minutes. Which of them has a rule of its own to protect?"
Priya, at the whiteboard
The prompt, and the kinds of question to ask
Priya gives the second problem: "Design a parking lot for a mall. Vehicles get a ticket at entry and pay at exit." Arjun remembers the five minutes of questions from the last problem. This time he sorts his questions into five kinds, so he never runs out of things to ask.
| Kind of question | What Arjun asks | Assumption |
|---|---|---|
| Scale | How big is it? | 3 floors, about 100 spots each, 2 entry gates, 2 exit gates |
| Kinds | Which vehicles? | Bikes, cars and large vehicles, each needing its own spot size |
| Rules | How is the fee worked out? | An hourly rate per type, rounded up, at least one hour |
| Failures | What if it is full? | Refuse entry with a clear error |
| At the same time | Can two gates work together? | Yes. Two cars must never get the same spot |
Scale tells you whether speed or concurrency matters. Kinds tell you what varies. Rules tell you what will change. Failures tell you which errors to design. The last kind is the one that people forget, and it decides the hardest part of this problem. Arjun also says aloud what he leaves out: real payments, reservations, number-plate cameras and databases.
From nouns and verbs to classes: how big is the first draft?
In the next five minutes Arjun draws fifteen boxes: ParkingLot, Floor, Spot, BikeSpot, CarSpot, LargeSpot, Vehicle, Car, Bike, Truck, Ticket, Gate, Payment, DisplayBoard and Attendant.
Priya gives him a test for each box: "Does it have data of its own, and a rule of its own to protect, inside the scope we agreed?" If both are true, it is a class. If only the first is true, it is a field or an enum.
- ParkingLot passes. It is the front door, and it owns the floors and the active tickets.
- ParkingFloor passes. It groups spots, and "nearest first" needs an order.
- ParkingSpot passes. It is free or taken, and it must hold at most one vehicle.
- BikeSpot, CarSpot and LargeSpot fail. They differ only by a size, so one
SpotTypeenum does the job. - Vehicle, Car, Bike and Truck pass. Each answers one question, "which spot do I need?", so the lot never asks "what kind are you?".
- Ticket passes. It holds the spot, the vehicle and the entry time.
- Gate fails. A gate only calls
parkandunpark, so it has no data or rule to protect. - Payment, DisplayBoard and Attendant are out of scope.
Subclasses or an enum? The three vehicles differ only in the spot they need, so an enum field would also work. Arjun keeps subclasses because the next vehicle may bring behaviour, such as an electric car that needs charging, and he says so aloud.
Eight boxes remain. That is the right size: every class has a reason to exist, and you can draw them in ten minutes. A draft that is too big hides the important decisions among boxes that do nothing. A draft that is too small, such as one class, hides them in a heap of code. Start a little smaller than you expect. You can split a class when a change request forces you. You cannot easily explain a class you never needed.
The first draft, and where it breaks
Arjun's first draft is the small end: one class, with plain strings for types.
public class NaiveParkingLot {
static class Spot {
String id;
String type; // "bike", "car" or "large"
String plate; // null when free
Spot(String id, String type) { this.id = id; this.type = type; }
}
private final List<Spot> spots = new ArrayList<>();
private final Map<String, Long> entryMinute = new HashMap<>();
public void addSpot(String id, String type) { spots.add(new Spot(id, type)); }
public String park(String plate, String type, long minute) throws InterruptedException {
Spot found = null;
for (Spot s : spots) {
if (s.plate == null && s.type.equals(type)) { found = s; break; }
}
if (found == null) return "FULL";
Thread.sleep(50); // the gate lifts its barrier here
found.plate = plate;
entryMinute.put(plate, minute);
return found.id;
}
public long unpark(String plate, String type, long minute) {
long hours = Math.max(1, (minute - entryMinute.remove(plate) + 59) / 60);
for (Spot s : spots) if (plate.equals(s.plate)) s.plate = null;
if (type.equals("bike")) return hours * 10;
else if (type.equals("car")) return hours * 20;
else return hours * 50;
}
}
It parks and charges. Then Priya adds pressure, one request at a time.
"Weekends cost double, and the first 15 minutes are free." Every rule lands in the if chain in unpark, and each new rule can break an old one. "A customer typed Car with a capital C." The lot answers FULL although it has empty spots, because the type is a plain string. Nothing warns you.
"Two gates, one free spot." The code finds a free spot, and a moment later it marks the spot taken. The Thread.sleep stands for the barrier lifting. Without it, the race is rare and shows up in production. With it, the race shows every time. Two threads, one per gate, park a car each in a lot with one spot:
KA01 -> A-1
KA02 -> A-1
Two cars hold a ticket for the same spot. This is a race condition: the result depends on which thread runs first.
Three decisions, each forced by a pain
Decision 1: the vehicle knows its own spot type. Strings go. The enum SpotType replaces them, and the vehicle answers for itself:
public abstract class Vehicle {
private final String number;
protected Vehicle(String number) { this.number = number; }
public String number() { return number; }
public abstract SpotType requiredSpot();
}
Car returns SpotType.CAR, Bike returns BIKE, and Truck returns LARGE. The lot has no if on vehicle type. A typo is now a compile error. An electric scooter is one new subclass.
Decision 2: the two rules that change get an interface each. The rule for choosing a spot and the rule for pricing both change often. This is the problem the strategy pattern solves: put each rule behind an interface and let the lot hold one.
public interface SpotFinder {
Optional<ParkingSpot> find(List<ParkingFloor> floors, SpotType type);
}
public interface FeeStrategy {
long fee(Ticket ticket, Instant exitTime); // rupees
}
NearestFirstFinder and HourlyFeeStrategy are the first implementations. The weekend and free-minutes rules become new classes that wrap an existing one, as in the decorator lesson. FirstFreeMinutes returns zero for short stays and asks the strategy inside it for longer ones. ParkingLot has not been edited. In Arjun's first run, two cars fill floor 1 and then floor 2, and a third car is refused. After 61 minutes the first car pays 40, which is 2 hours at 20 rupees. The freed spot goes to the next car. A 10 minute stay under FirstFreeMinutes with 15 free minutes pays 0.
KA01 parked in 1-C1, ticket T1
KA02 parked in 2-C1, ticket T2
KA03 refused: No free CAR spot
T1 pays 40
KA03 parked in 1-C1, ticket T3
classDiagram
class ParkingLot {
+park(v) Ticket
+unpark(id) long
}
class SpotFinder {
<<interface>>
}
class FeeStrategy {
<<interface>>
}
ParkingLot *-- ParkingFloor
ParkingFloor *-- ParkingSpot
ParkingLot --> SpotFinder
ParkingLot --> FeeStrategy
SpotFinder <|.. NearestFirstFinder
FeeStrategy <|.. HourlyFeeStrategy
FeeStrategy <|.. FirstFreeMinutes
Decision 3: lock the spot, not the lot. Where exactly is the race? Two gates may both find spot 1-C1 free, because finding changes nothing. The danger is the step that takes it. So that step becomes one indivisible action, and it answers "did I get it?":
public synchronized boolean tryAssign(Vehicle v) {
if (vehicle != null) return false;
vehicle = v;
return true;
}
synchronized lets one thread at a time into the method on the same spot, so check and write happen together. Think of two people reaching for the last chair in a row. Whoever sits first owns it, and the other looks for another chair. The picture stops working here: people see each other, but threads do not, so the check and the sit must be one step. ParkingLot.park loops: find a spot, try to take it, and if another gate won, find again.
while (true) {
ParkingSpot spot = finder.find(floors, type)
.orElseThrow(() -> new LotFullException(type));
if (spot.tryAssign(vehicle)) {
Ticket ticket = new Ticket("T" + nextTicket.getAndIncrement(), vehicle, spot, clock.instant());
active.put(ticket.id(), ticket);
return ticket;
}
}
A lock on the whole park method also works and is simpler. It makes every gate wait for every other gate, even on different floors. The spot lock costs one retry loop and lets gates that want different spots run side by side. Say which you chose and why. Arjun reruns the race with a pause placed between find and assign, in a lot with one car spot and then in a lot with two:
-- two gates, one car spot
spots given out [1-C1], cars refused 1
-- two gates, two car spots
spots given out [1-C1, 1-C2], cars refused 0
With one spot, one car wins and the other gets LotFullException. With two, the loser of the first round finds the second spot on its retry. No spot is ever given twice.
Extend it, then break it
"Bikes may use a car spot when every bike spot is full." Arjun changes no existing class. A wrapper finder tries the normal type first, then a fallback type:
@Override public Optional<ParkingSpot> find(List<ParkingFloor> floors, SpotType type) {
Optional<ParkingSpot> spot = base.find(floors, type);
if (spot.isEmpty() && fallback.containsKey(type)) return base.find(floors, fallback.get(type));
return spot;
}
Priya asks the follow-up that matters: "What does that bike pay?" HourlyFeeStrategy reads the rate from ticket.vehicle().requiredSpot(), so the bike pays the bike rate even in a car spot. In the demo, the second bike took 1-C1 for 30 minutes and paid 10. Pricing by the spot type would have charged 20 and surprised the customer. Choose on purpose which one you price by.
"Someone scans the same ticket at two exit gates." active.remove(ticketId) returns the ticket once, to one caller. The second caller gets null and a clear error: Unknown or used ticket: T1. The map is a ConcurrentHashMap, whose remove is atomic, so no lock is needed.
⚠️ Warning: Do not read the clock inside
FeeStrategy. GiveParkingLotaClock, and tests can move time by hand. The demo here moves it by 61 minutes to charge 40 rupees for 2 hours of car parking.
Your turn: pull the two rules out
Start from a lot that finds spots and prices tickets inside one class. Move both rules behind interfaces, add one implementation of each, and let the lot hold them.
📌 Summary: Make a class only when a noun has data and a rule of its own. Put each changing rule behind an interface. Lock the shared spot, and retry when another gate wins.
Next, open the problem Parking Lot in the problem library. Design it again from a blank page, compare it with the model answer, and then use its "Build it on your machine" kit to write and run it.
What Arjun learns next
Parking has a few states, but a spot is only free or taken. The next problem is a machine whose same button does different things depending on what happened before. Priya hands Arjun a vending machine, and a first draft full of if (state == ...).
Check your understanding
- Arjun drew fifteen boxes. Priya's test is "data of its own and a rule of its own to protect". Which box should not become a class in the first draft?
- The mall asks for weekend rates next month, then a daily cap the month after. Where does each change go?
- Gate 1 and gate 2 both find spot 1-C1 free at the same moment. What makes sure only one car gets it?
- Priya wants bikes in car spots when bike spots are full, and the bike must still pay the bike rate. What changes?