Design a Parking Lot
A classic object-oriented design question - model vehicles, spots, levels, tickets and pricing with clean abstractions and thread-safe spot assignment.
Requirements
Low-level design interviews test whether you can turn fuzzy requirements into clean classes, responsibilities and relationships. Start by pinning the scope.
Functional
- The lot has multiple levels; each level has spots of different sizes (small, compact, large).
- Supports motorcycles, cars and trucks. A vehicle fits any spot at least as big as it needs:
- Motorcycle → small, compact or large
- Car → compact or large
- Truck → large only
- On entry, assign a spot and issue a ticket; reject the vehicle if the lot is full.
- On exit, compute the fee from the ticket and free the spot.
- Show availability per spot size.
Non-functional
- Multiple entry and exit gates operate concurrently; two cars must never get the same spot.
- Pricing and spot-assignment rules should be swappable without touching core classes.
- Easy to extend with new vehicle types (e.g. electric vehicles needing chargers).
Out of scope (confirm with the interviewer)
- Payment processing, reservations, license-plate cameras, persistence.
Say your assumptions out loud: "one vehicle per spot, a truck takes one large spot, fees per started hour". It is far better than silently picking a complex variant like multi-spot trucks.
Core Entities
| Entity | Responsibility | Key fields |
|---|---|---|
Vehicle (abstract) |
Knows its plate, type and required spot size | licensePlate, type |
Motorcycle, Car, Truck |
Concrete vehicles | override requiredSize() |
ParkingSpot |
One physical spot; atomic assign/release | id, size, vehicle |
Level |
Groups spots on one floor; reports free counts | number, spots |
Ticket |
Proof of entry; links vehicle, spot and time | id, vehicle, spot, entryTime |
SpotAssignmentStrategy |
Chooses which free spot to use | interface |
PricingStrategy |
Computes the fee for a ticket | interface |
ParkingLot |
Facade: park, unpark, availability; owns active tickets | levels, strategies, activeTickets |
VehicleFactory |
Creates the right Vehicle subtype from a type |
static create() |
Notice what is not an entity: Gate, Payment, DisplayBoard. Add them only if the interviewer asks; they hang off ParkingLot without changing the core.
Class Diagram
classDiagram
class Vehicle {
<<abstract>>
-String licensePlate
-VehicleType type
+requiredSize() SpotSize
}
class Motorcycle
class Car
class Truck
Vehicle <|-- Motorcycle
Vehicle <|-- Car
Vehicle <|-- Truck
class ParkingSpot {
-String id
-SpotSize size
-Vehicle vehicle
+canFit(Vehicle) boolean
+assign(Vehicle) boolean
+release() void
}
class Level {
-int number
-List~ParkingSpot~ spots
+freeCount(SpotSize) long
}
class Ticket {
-String id
-Vehicle vehicle
-ParkingSpot spot
-LocalDateTime entryTime
}
class ParkingLot {
-List~Level~ levels
-Map activeTickets
+park(Vehicle, LocalDateTime) Optional~Ticket~
+unpark(String, LocalDateTime) long
+availability() Map
}
class SpotAssignmentStrategy {
<<interface>>
+findSpot(List~Level~, Vehicle) Optional~ParkingSpot~
}
class PricingStrategy {
<<interface>>
+feeInCents(Ticket, LocalDateTime) long
}
class BestFitLowestLevelStrategy
class HourlyPricingStrategy
class VehicleFactory {
+create(VehicleType, String) Vehicle
}
ParkingLot "1" *-- "many" Level
Level "1" *-- "many" ParkingSpot
ParkingLot "1" o-- "many" Ticket
Ticket --> Vehicle
Ticket --> ParkingSpot
ParkingSpot --> Vehicle
ParkingLot --> SpotAssignmentStrategy
ParkingLot --> PricingStrategy
SpotAssignmentStrategy <|.. BestFitLowestLevelStrategy
PricingStrategy <|.. HourlyPricingStrategy
VehicleFactory ..> Vehicle : creates
Design Patterns Used
| Pattern | Where | Why it is justified |
|---|---|---|
| Strategy | PricingStrategy, SpotAssignmentStrategy |
Pricing (hourly, flat, weekend, surge) and assignment (nearest to gate, best fit, EV-first) change often; new rules become new classes, not if chains |
| Factory | VehicleFactory |
Gates receive a vehicle type from a sensor or UI; one place maps type → subtype |
| Facade | ParkingLot |
Clients call park / unpark without knowing about levels or spots |
| Polymorphism over conditionals | Vehicle.requiredSize() |
Each vehicle knows what it needs; adding a vehicle type does not modify spot logic |
What about Singleton? Interviewers often expect ParkingLot.getInstance(). A real system has exactly one lot per process, but a hard-coded Singleton makes testing harder (global state, no fresh instance per test) and breaks if the company later manages several lots. A better answer: "one instance, created at startup and passed in via dependency injection". Mention the trade-off rather than reflexively using the pattern.
canFit compares enum ordinals (SMALL < COMPACT < LARGE), so "fits in this size or larger" is one line. The best-fit strategy then picks the smallest fitting spot, keeping large spots free for trucks.
Java Implementation
The code compiles as a single Main.java (Java 17+). Times are passed in explicitly so the demo is deterministic; production code would inject a java.time.Clock. Money is held in cents to avoid floating-point rounding.
import java.time.Duration; import java.time.LocalDateTime; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicLong; enum VehicleType { MOTORCYCLE, CAR, TRUCK } enum SpotSize { SMALL, COMPACT, LARGE } // ordered smallest to largest abstract class Vehicle { private final String licensePlate; private final VehicleType type; protected Vehicle(String licensePlate, VehicleType type) { this.licensePlate = licensePlate; this.type = type; } String getLicensePlate() { return licensePlate; } VehicleType getType() { return type; } abstract SpotSize requiredSize(); @Override public String toString() { return type + "(" + licensePlate + ")"; } } class Motorcycle extends Vehicle { Motorcycle(String plate) { super(plate, VehicleType.MOTORCYCLE); } SpotSize requiredSize() { return SpotSize.SMALL; } } class Car extends Vehicle { Car(String plate) { super(plate, VehicleType.CAR); } SpotSize requiredSize() { return SpotSize.COMPACT; } } class Truck extends Vehicle { Truck(String plate) { super(plate, VehicleType.TRUCK); } SpotSize requiredSize() { return SpotSize.LARGE; } } final class VehicleFactory { private VehicleFactory() {} static Vehicle create(VehicleType type, String plate) { return switch (type) { case MOTORCYCLE -> new Motorcycle(plate); case CAR -> new Car(plate); case TRUCK -> new Truck(plate); }; } } class ParkingSpot { private final String id; private final SpotSize size; private Vehicle vehicle; ParkingSpot(String id, SpotSize size) { this.id = id; this.size = size; } String getId() { return id; } SpotSize getSize() { return size; } synchronized boolean isFree() { return vehicle == null; } boolean canFit(Vehicle v) { return size.compareTo(v.requiredSize()) >= 0; } // Atomic check-and-set: only one thread can win a given spot. synchronized boolean assign(Vehicle v) { if (vehicle != null || !canFit(v)) return false; vehicle = v; return true; } synchronized void release() { vehicle = null; } } class Level { private final int number; private final List<ParkingSpot> spots = new ArrayList<>(); Level(int number, int small, int compact, int large) { this.number = number; addSpots(SpotSize.SMALL, small); addSpots(SpotSize.COMPACT, compact); addSpots(SpotSize.LARGE, large); } private void addSpots(SpotSize size, int count) { for (int i = 0; i < count; i++) { spots.add(new ParkingSpot("L" + number + "-" + size.name().charAt(0) + (i + 1), size)); } } int getNumber() { return number; } List<ParkingSpot> getSpots() { return Collections.unmodifiableList(spots); } long freeCount(SpotSize size) { return spots.stream().filter(s -> s.getSize() == size && s.isFree()).count(); } } interface SpotAssignmentStrategy { Optional<ParkingSpot> findSpot(List<Level> levels, Vehicle vehicle); } // Lowest level first; within a level, the smallest spot that fits (keeps big spots for big vehicles). class BestFitLowestLevelStrategy implements SpotAssignmentStrategy { public Optional<ParkingSpot> findSpot(List<Level> levels, Vehicle vehicle) { for (Level level : levels) { Optional<ParkingSpot> spot = level.getSpots().stream() .filter(s -> s.isFree() && s.canFit(vehicle)) .min(Comparator.comparing(ParkingSpot::getSize)); if (spot.isPresent()) return spot; } return Optional.empty(); } } class Ticket { private final String id; private final Vehicle vehicle; private final ParkingSpot spot; private final LocalDateTime entryTime; Ticket(String id, Vehicle vehicle, ParkingSpot spot, LocalDateTime entryTime) { this.id = id; this.vehicle = vehicle; this.spot = spot; this.entryTime = entryTime; } String getId() { return id; } Vehicle getVehicle() { return vehicle; } ParkingSpot getSpot() { return spot; } LocalDateTime getEntryTime() { return entryTime; } } interface PricingStrategy { long feeInCents(Ticket ticket, LocalDateTime exitTime); } // Charges per started hour, minimum one hour, with a rate per vehicle type. class HourlyPricingStrategy implements PricingStrategy { private final Map<VehicleType, Long> centsPerHour = new EnumMap<>(VehicleType.class); HourlyPricingStrategy() { centsPerHour.put(VehicleType.MOTORCYCLE, 100L); centsPerHour.put(VehicleType.CAR, 250L); centsPerHour.put(VehicleType.TRUCK, 500L); } public long feeInCents(Ticket ticket, LocalDateTime exitTime) { long minutes = Duration.between(ticket.getEntryTime(), exitTime).toMinutes(); long hours = Math.max(1, (minutes + 59) / 60); return hours * centsPerHour.get(ticket.getVehicle().getType()); } } class ParkingLot { private final String name; private final List<Level> levels; private final SpotAssignmentStrategy assignmentStrategy; private final PricingStrategy pricingStrategy; private final Map<String, Ticket> activeTickets = new ConcurrentHashMap<>(); private final AtomicLong ticketSequence = new AtomicLong(); ParkingLot(String name, List<Level> levels, SpotAssignmentStrategy assignmentStrategy, PricingStrategy pricingStrategy) { this.name = name; this.levels = List.copyOf(levels); this.assignmentStrategy = assignmentStrategy; this.pricingStrategy = pricingStrategy; } Optional<Ticket> park(Vehicle vehicle, LocalDateTime entryTime) { while (true) { Optional<ParkingSpot> candidate = assignmentStrategy.findSpot(levels, vehicle); if (candidate.isEmpty()) return Optional.empty(); ParkingSpot spot = candidate.get(); if (spot.assign(vehicle)) { // lost the race? loop and pick another spot String id = "T" + ticketSequence.incrementAndGet(); Ticket ticket = new Ticket(id, vehicle, spot, entryTime); activeTickets.put(id, ticket); return Optional.of(ticket); } } } long unpark(String ticketId, LocalDateTime exitTime) { Ticket ticket = activeTickets.remove(ticketId); if (ticket == null) throw new IllegalArgumentException("Unknown or used ticket: " + ticketId); long fee = pricingStrategy.feeInCents(ticket, exitTime); ticket.getSpot().release(); return fee; } Map<SpotSize, Long> availability() { Map<SpotSize, Long> free = new EnumMap<>(SpotSize.class); for (SpotSize size : SpotSize.values()) { free.put(size, levels.stream().mapToLong(l -> l.freeCount(size)).sum()); } return free; } String getName() { return name; } } public class Main { public static void main(String[] args) { ParkingLot lot = new ParkingLot("City Center", List.of(new Level(0, 1, 1, 1), new Level(1, 0, 2, 1)), new BestFitLowestLevelStrategy(), new HourlyPricingStrategy()); System.out.println(lot.getName() + " free spots: " + lot.availability()); LocalDateTime t0 = LocalDateTime.of(2026, 10, 29, 9, 0); List<Vehicle> arrivals = List.of( VehicleFactory.create(VehicleType.MOTORCYCLE, "MC-1"), VehicleFactory.create(VehicleType.CAR, "CAR-1"), VehicleFactory.create(VehicleType.TRUCK, "TRK-1"), VehicleFactory.create(VehicleType.CAR, "CAR-2"), VehicleFactory.create(VehicleType.TRUCK, "TRK-2"), VehicleFactory.create(VehicleType.TRUCK, "TRK-3")); Map<String, Ticket> tickets = new LinkedHashMap<>(); for (Vehicle v : arrivals) { Optional<Ticket> ticket = lot.park(v, t0); if (ticket.isPresent()) { Ticket t = ticket.get(); tickets.put(v.getLicensePlate(), t); System.out.println("Parked " + v + " at " + t.getSpot().getId() + " ticket " + t.getId()); } else { System.out.println("No spot for " + v); } } System.out.println("Free spots: " + lot.availability()); long carFee = lot.unpark(tickets.get("CAR-1").getId(), t0.plusMinutes(95)); long truckFee = lot.unpark(tickets.get("TRK-1").getId(), t0.plusMinutes(30)); System.out.printf("CAR-1 paid $%.2f after 95 min%n", carFee / 100.0); System.out.printf("TRK-1 paid $%.2f after 30 min%n", truckFee / 100.0); Optional<Ticket> retry = lot.park(VehicleFactory.create(VehicleType.TRUCK, "TRK-3"), t0.plusMinutes(31)); System.out.println("Retry TRK-3: " + retry.map(t -> "parked at " + t.getSpot().getId()).orElse("still full")); System.out.println("Free spots: " + lot.availability()); try { lot.unpark(tickets.get("CAR-1").getId(), t0.plusHours(3)); } catch (IllegalArgumentException e) { System.out.println("Rejected: " + e.getMessage()); } } }
Demo output
City Center free spots: {SMALL=1, COMPACT=3, LARGE=2}
Parked MOTORCYCLE(MC-1) at L0-S1 ticket T1
Parked CAR(CAR-1) at L0-C1 ticket T2
Parked TRUCK(TRK-1) at L0-L1 ticket T3
Parked CAR(CAR-2) at L1-C1 ticket T4
Parked TRUCK(TRK-2) at L1-L1 ticket T5
No spot for TRUCK(TRK-3)
Free spots: {SMALL=0, COMPACT=1, LARGE=0}
CAR-1 paid $5.00 after 95 min
TRK-1 paid $5.00 after 30 min
Retry TRK-3: parked at L0-L1
Free spots: {SMALL=0, COMPACT=2, LARGE=0}
Rejected: Unknown or used ticket: T2
Walkthrough of the key decisions:
- The motorcycle takes the small spot and the first car the compact spot on level 0: best fit, lowest level first.
- The third truck is rejected because both large spots are taken, even though a compact spot is free.
- Fees: 95 minutes is 2 started hours × $2.50 = $5.00; a 30-minute truck stay is billed the 1-hour minimum of $5.00.
- Reusing a ticket fails because
unparkremoves it fromactiveTicketsatomically.
Concurrency Considerations
Multiple gates call park simultaneously. The danger is a check-then-act race: two threads both see spot L0-C1 as free and both take it.
How the design prevents it:
ParkingSpot.assignissynchronizedand re-checksvehicle == nullbefore setting it, an atomic compare-and-set per spot.parktreats the strategy's choice as a candidate. Ifassignfails because another gate won, it loops and asks for another spot.- Lock granularity is one spot, so gates rarely block each other. A single lot-wide lock would be simpler but serializes every entry.
activeTicketsis aConcurrentHashMap, andremovemakes ticket redemption idempotent: a double-scanned ticket is rejected, not charged twice.AtomicLonggenerates unique ticket IDs without locking.
Scanning every spot on every park is O(total spots). For large lots, keep per-level, per-size free lists (e.g. a ConcurrentLinkedQueue<ParkingSpot> per size) so finding a spot is O(1). Then polling the queue is the atomic claim, and release offers the spot back.
Other points to raise:
- Availability counters may be momentarily stale for the display board; that is acceptable (eventual consistency) as long as assignment itself is atomic.
- In a distributed setup (several gate servers, shared database), the per-spot lock becomes a conditional update:
UPDATE spot SET vehicle_id = ? WHERE id = ? AND vehicle_id IS NULL, checking that one row changed.
Extensions
| Requirement | Change |
|---|---|
| Electric vehicles with chargers | Add ElectricCar and an EV_CHARGING spot type or a hasCharger flag; an EvFirstStrategy prefers charger spots |
| Different pricing (weekend, flat daily cap, membership) | New PricingStrategy implementations; compose with a decorator for discounts |
| Nearest spot to entry gate | SpotAssignmentStrategy that takes the gate and uses distances or a priority queue per gate |
| Display boards per level | Observer: Level publishes spot changes; boards subscribe |
| Reservations | Reservation entity holding a spot for a time window; assignment skips reserved spots |
| Payments | PaymentProcessor interface (card, cash, wallet) called after fee calculation |
| Large vehicles spanning multiple spots | Assign a contiguous run of spots atomically (lock spots in ID order to avoid deadlock) |
Interview Tips
- Spend 3–5 minutes on requirements and state assumptions; then list entities before writing code.
- Draw the class diagram first; interviewers grade relationships (composition vs association) as much as code.
- Use enums for fixed categories, an abstract class for vehicles, and interfaces for pluggable policy.
- Justify every pattern. "Strategy because pricing changes often" beats naming patterns for their own sake.
- Bring up concurrency yourself: the double-assignment race and how per-spot locking fixes it.
- Keep money in integer cents (or
BigDecimal) and inject time, which shows production instincts. - Close with extensions to show the design is open for change without modifying core classes.