On this page
Description
"Design a parking lot" is the most common object-oriented design prompt, and a good warm-up for the whole family: a few entities, a couple of rules, two lookups that must stay in sync, and follow-ups (vehicle sizes, pricing, several gates) that test whether the design can absorb change. This skill walks through the clarifying questions, the model, the core data structures and the allocation policy.
"Design a parking lot" is the most common object-oriented design prompt, and the first attempt usually puts everything into one class that scans:
Java
class ParkingLot {
boolean[] occupied = new boolean[500];
String[] plateAt = new String[500];
int park(String plate) {
for (int i = 0; i < occupied.length; i++)
if (!occupied[i]) { occupied[i] = true; plateAt[i] = plate; return i; }
return -1;
}
void leave(String plate) {
for (int i = 0; i < plateAt.length; i++)
if (plate.equals(plateAt[i])) { occupied[i] = false; plateAt[i] = null; }
}
}
Python
class ParkingLot:
def __init__(self):
self.occupied = [False] * 500
self.plate_at: list[str | None] = [None] * 500
def park(self, plate: str) -> int:
for i in range(len(self.occupied)):
if not self.occupied[i]:
self.occupied[i] = True
self.plate_at[i] = plate
return i
return -1
def leave(self, plate: str) -> None:
for i in range(len(self.plate_at)):
if self.plate_at[i] == plate:
self.occupied[i] = False
self.plate_at[i] = None
It passes a demo, and every follow-up breaks it. Vehicle sizes (a bus can't use a motorcycle bay), a second entrance, a price per hour, a board showing free spots per size - each one lands as another array and another if inside the same two loops. leave by plate frees every spot holding that plate, nothing stops the same car being parked twice, and nobody outside the class can tell which parking session a spot belongs to.
We'll go from simple to more technical:
- The four steps - clarify, model, API, core - applied to a garage.
- The core: free spots per size and a ticket-to-spot map.
- Allocation as a swappable policy, and smallest fit first.
- What a real garage adds, and the bugs that come from two structures disagreeing.
The approach
An object-oriented design round rewards the same four steps every time - clarify, model, define the API, build the core - with extension points where the interviewer is likely to push next.
- Clarify. Which vehicle types and spot sizes? May a small vehicle use a bigger spot? One level or many, one gate or several? What happens when nothing fits - return a value or throw? Is pricing in scope? Is it concurrent?
- Model. Entities with identity and a lifecycle (
ParkingSpot, a ticket), value objects (Vehicle: a type and a plate), enums for closed sets (Vehicle.Type,ParkingSpot.Size), and one facade that owns the rules (ParkingLot). - API. Three operations:
String parkVehicle(Vehicle)returns a ticket, ornullwhen nothing fits;boolean unparkVehicle(String ticketId);int availableSpots(ParkingSpot.Size). - Core. Two lookups that run in opposite directions, each with its own structure: a free spot of this size for park, and ticket → spot for unpark.
ParkingLot ──owns──> ParkingSpot (size, occupied) Vehicle (type, plate)
│ ^
│ asks │ ticket id -> spot
v │
FitPolicy Map<String, ParkingSpot>
The pieces in code
Java
public class ParkingLot {
private final Map<ParkingSpot.Size, Deque<ParkingSpot>> free = new EnumMap<>(ParkingSpot.Size.class);
private final Map<String, ParkingSpot> spotByTicket = new HashMap<>();
public ParkingLot(List<ParkingSpot> spots) {
for (ParkingSpot.Size s : ParkingSpot.Size.values()) free.put(s, new ArrayDeque<>());
for (ParkingSpot spot : spots) free.get(spot.getSize()).addLast(spot);
}
public String parkVehicle(Vehicle vehicle) {
ParkingSpot spot = free.get(sizeFor(vehicle.getType())).pollFirst(); // O(1); null when that size is full
if (spot == null) return null;
spot.setOccupied(true);
String ticket = UUID.randomUUID().toString();
spotByTicket.put(ticket, spot);
return ticket;
}
public boolean unparkVehicle(String ticketId) {
ParkingSpot spot = spotByTicket.remove(ticketId); // remove, not get: a ticket works once
if (spot == null) return false;
spot.setOccupied(false);
free.get(spot.getSize()).addLast(spot);
return true;
}
public int availableSpots(ParkingSpot.Size size) {
return free.get(size).size(); // the free list is the count
}
}
Python
import uuid
from collections import deque
class ParkingLot:
def __init__(self, spots: list[ParkingSpot]):
self._free: dict[ParkingSpot.Size, deque[ParkingSpot]] = {s: deque() for s in ParkingSpot.Size}
self._spot_by_ticket: dict[str, ParkingSpot] = {}
for spot in spots:
self._free[spot.size].append(spot)
def park_vehicle(self, vehicle: Vehicle) -> str | None:
free = self._free[size_for(vehicle.type)]
if not free:
return None # that size is full
spot = free.popleft() # O(1)
spot.occupied = True
ticket = str(uuid.uuid4())
self._spot_by_ticket[ticket] = spot
return ticket
def unpark_vehicle(self, ticket_id: str) -> bool:
spot = self._spot_by_ticket.pop(ticket_id, None) # pop, not get: a ticket works once
if spot is None:
return False
spot.occupied = False
self._free[spot.size].append(spot)
return True
def available_spots(self, size: ParkingSpot.Size) -> int:
return len(self._free[size]) # the free list is the count
(sizeFor is a switch mapping MOTORCYCLE, CAR and BUS to SMALL, MEDIUM and LARGE.) Three details carry most of the grading:
remove, notget, inunparkVehicle(in Python,pop, notget) makes a ticket single-use. Withget, a second call returnstrueand appends the spot to the free list again - now it's listed twice, and two later cars are handed the same spot.availableSpotsreads the free list's size instead of a separate counter, so the number can't drift from the truth.pollFirst()returnsnullon an empty deque (removeFirst()would throw), which is exactly "nothing of that size is free". (Python'sdeque.popleft()raisesIndexErroron an empty deque instead - hence theif not freecheck first.)
A ticket, not a plate, is the key: it names one parking session. The lot mints it, can refuse it once used, and can later carry the entry time a price needs. A plate names a car, and the same car comes back tomorrow.
Allocation is a policy
"Which spot?" changes more often than anything else in this design: exact size only, a motorcycle allowed in a car bay, the bay nearest the gate you came in through. Keep that decision behind one small interface and let ParkingLot keep its structures private:
Java
public interface FitPolicy {
List<ParkingSpot.Size> sizesToTry(Vehicle.Type type); // in preference order
FitPolicy EXACT = type -> List.of(switch (type) {
case MOTORCYCLE -> SMALL;
case CAR -> MEDIUM;
case BUS -> LARGE;
});
FitPolicy SMALLEST_FIRST = type -> switch (type) {
case MOTORCYCLE -> List.of(SMALL, MEDIUM, LARGE);
case CAR -> List.of(MEDIUM, LARGE);
case BUS -> List.of(LARGE);
};
}
Python
from collections.abc import Callable
# A fit policy: the spot sizes to try for a vehicle type, in preference order.
type FitPolicy = Callable[[Vehicle.Type], list[ParkingSpot.Size]]
def exact(t: Vehicle.Type) -> list[ParkingSpot.Size]:
return [{MOTORCYCLE: SMALL, CAR: MEDIUM, BUS: LARGE}[t]]
def smallest_first(t: Vehicle.Type) -> list[ParkingSpot.Size]:
return {MOTORCYCLE: [SMALL, MEDIUM, LARGE], CAR: [MEDIUM, LARGE], BUS: [LARGE]}[t]
parkVehicle then walks fitPolicy.sizesToTry(vehicle.getType()) and takes the first spot it can poll. (In Python a one-method policy is simply a function, as above.) Smallest fit first matters as soon as vehicles may use bigger bays: giving a motorcycle the lowest-numbered free spot - which might be a bus bay - can turn the next bus away while small bays stand empty.
Not every policy fits this shape. "Nearest to your gate" orders spots within a size, so it changes the structure too: each gate needs free spots ordered by its own distance, such as a PriorityQueue per gate. That is one reason the free lists stay private - the policy says what to prefer, and the lot decides how to store spots so that preference is cheap.
What a real garage adds
A real garage adds what the interview version leaves out: tickets are stored durably (a restart must not forget who is parked), the entry time on the ticket feeds a pricing component at the exit, per-bay sensors report occupancy that can disagree with the software (a car parks across two bays; a sensor fails), and display boards subscribe to free-count changes rather than polling. Each of those is a new class that talks to the lot through a narrow interface - the lot itself stays about space.
Which tool for which job?
| You need | Use |
|---|---|
| A free spot of a given size, fast | a deque (free list) per size |
| The spot behind a ticket | a map from ticket id to spot |
| A ticket that works once | remove / pop, never get |
| Rules for which sizes a vehicle may use | a fit policy (interface or function) |
| Nearest spot to a gate | a priority queue per gate |
| Free-count boards | the free list's size, published to observers |
Common mistakes
| Mistake | What happens | Do this instead |
|---|---|---|
get instead of remove on unpark | the spot is freed twice; two cars share it | remove the ticket on use |
| Returning the live spot list | callers flip occupied behind the lot's back | return copies or counts |
| A separate counter lowered before a spot is found | the free count drifts, even below zero | read the free list's size |
| Keying sessions by plate | the same car tomorrow collides with today | a ticket per parking session |
| A singleton lot | shared state in tests; two garages impossible | one instance, passed in |
Check yourself
Try to answer before opening each one - recalling beats re-reading.
1. What test would catch an unparkVehicle that reads the ticket map with get instead of remove?
Show answer
Unpark the same ticket twice and expect false the second time - the get version returns true. A stronger test shows the damage: with two spots, park A, unpark A twice, then park three more cars - with the bug, the free list holds A's spot twice and two of the new cars are given the same spot instead of the third being turned away.
2. Motorcycles may now use any free spot. Why try SMALL before LARGE instead of taking the lowest-numbered free spot of any size?
Show answer
Because big bays are scarce and only big vehicles need them. If the lowest-numbered free spot happens to be a bus bay, a motorcycle takes it and the next bus is turned away even though small bays are empty. Smallest fit first keeps each size free for the vehicles that can't go anywhere else.
3. Two exits scan the same ticket at the same instant, and tickets live in a ConcurrentHashMap. What makes the spot come free only once?
Show answer
remove(ticketId) is atomic: exactly one of the two calls gets the spot back, and the other gets null and returns false. Only the caller that received the spot marks it free and returns it to the free list. A get followed by a separate remove would reopen the race, because both exits could read the spot before either removes it.
Key takeaways
- Clarify, model, define the API, then build the core - the clarifying answers decide the entities and structures.
- A parking lot needs two lookups in opposite directions: a free spot by size for park, and ticket → spot for unpark. Keep them private and update them together.
- A ticket names a parking session: mint it, and remove it on use so it works exactly once.
- Allocation rules change most often: put them behind one small interface, and prefer the smallest size that fits.
- Under concurrency, finding and taking a spot must be one atomic step - a lock, or structures whose
poll/removehand an item to exactly one caller.