Design Parking Lot
The #1 LLD warm-up at Amazon, Microsoft, Flipkart, and most service companies — OOP, enums, spot allocation, and pricing in one clean design.
Model vehicles, spots (size-matched), floors, and a ParkingLot facade. Use Strategy for fees, Factory for vehicles, and keep APIs idempotent for entry/exit. Interviewers want clarifying questions first, then working code — not UML perfection.
The interview question
“Design a parking lot system.” You are usually asked to support multiple vehicle types, multiple floors, park / unpark, and fee on exit.
At product companies the bar is: ask scope, draw 4–6 classes, then code the happy path in Java (or language of choice) with room to extend.
Clarifying questions (ask before coding)
Spend 3–5 minutes here. Interviewers score communication; silence loses points.
if chains.Requirements & assumptions (state aloud)
- Functional: park vehicle → issue ticket; unpark with ticket → compute fee; find nearest free compatible spot.
- Vehicle types:
BIKE,CAR,TRUCKmapped to spot sizesSMALL,MEDIUM,LARGE. - Structure: one
ParkingLotwith NFloors; each floor has a list ofParkingSpots. - Non-functional (v1): single process, in-memory, thread-safe spot booking optional.
- Out of scope (v1): payment gateway, ANPR cameras, reservations, EV charging — list as extensions.
| Scope | In scope (v1) | Extension (mention) |
|---|---|---|
| Allocation | First free spot matching size | Nearest entrance, handicap spots |
| Pricing | Hourly rate per vehicle type | Weekend surge, membership |
| Scale | One building | Multi-lot central billing |
High-level design & class relationships
Keep ~6–8 classes visible on the whiteboard. ParkingLot is the facade interviewers interact with.
Spot allocation: scan floors in order; first ParkingSpot where spot.canFit(vehicle) and !spot.isOccupied().
- Enums:
VehicleType,SpotSize,SpotStatus - Domain:
Vehicle,ParkingSpot,Floor,Ticket - Services:
ParkingLot(park/unpark),PricingStrategy - Factory:
VehicleFactory(optional but liked)
🟢 free 🔴 occupied — S/M/L/B = spot size for Car / medium / Truck / Bike
UML class diagram (optional — if interviewer asks)
Not every interview requires a class diagram. Many interviewers want code first; others (especially Microsoft, Oracle, some Amazon loops) say *“draw the classes before you code.”*
When to draw: after clarifying requirements, before or in parallel with coding — takes 3–5 minutes. Say: *“I’ll sketch the main classes and relationships, then implement the facade.”*
What to include: 6–8 classes max, key fields, 2–3 methods per class, and relationships (composition > association > dependency). Skip getters/setters on the board.
Relationships to label aloud
- Composition (◆):
ParkingLot→Floor→ParkingSpot— spots do not exist without the lot; lifecycle tied together. - Association:
Ticket→Vehicle+ParkingSpot— ticket references what was parked and where. - Dependency (- -▷):
ParkingLotusesPricingStrategy;VehicleFactorycreatesVehicle. - Implementation (△):
HourlyPricingStrategyimplementsPricingStrategy.
If interviewer says “simplify”
- Drop
VehicleFactoryand constructVehicledirectly. - Merge enums into comments instead of separate enum boxes.
- Keep only:
ParkingLot,Floor,ParkingSpot,Vehicle,Ticket,PricingStrategy.
Design patterns used (and why)
| Pattern | Where | Why (interview soundbite) |
|---|---|---|
| 🏭Factory Method | VehicleFactory.create(type, plate) | Centralizes vehicle creation; adding EV_CAR does not touch ParkingLot. |
| 🎯Strategy | PricingStrategy → HourlyPricingStrategy | Swap hourly / flat / dynamic pricing without changing exit flow. |
| 🔒Singleton (optional) | ParkingLot.getInstance() | One physical lot per JVM — mention trade-off: hurts testing unless you allow injection. |
| 🪞Facade | ParkingLot public API | Hides floors/spots/tickets — “park this car” is one call for clients/gates. |
| 🎬State (light) | ParkingSpot FREE ↔ OCCUPIED | Illegal transitions (park twice) are rejected inside the spot. |
Full solution — enums & domain model
public enum VehicleType { BIKE, CAR, TRUCK }
public enum SpotSize { SMALL, MEDIUM, LARGE }
public enum SpotStatus { FREE, OCCUPIED }
public final class VehicleSizeMapper {
// Private ctor — utility class, no instances
private VehicleSizeMapper() {}
// Returns smallest spot size that can fit this vehicle type
public static SpotSize requiredSpot(VehicleType type) {
return switch (type) {
case BIKE -> SpotSize.SMALL;
case CAR -> SpotSize.MEDIUM;
case TRUCK -> SpotSize.LARGE;
};
}
}
public class Vehicle {
private final String licensePlate;
private final VehicleType type;
// Creates an immutable vehicle with plate and type
public Vehicle(String licensePlate, VehicleType type) {
this.licensePlate = Objects.requireNonNull(licensePlate);
this.type = Objects.requireNonNull(type);
}
// Returns license plate for ticket / duplicate-entry checks
public String getLicensePlate() { return licensePlate; }
// Returns vehicle type for spot-size and pricing lookups
public VehicleType getType() { return type; }
}public class ParkingSpot {
private final String id;
private final SpotSize size;
private SpotStatus status = SpotStatus.FREE;
private Vehicle parkedVehicle;
// Creates a spot with fixed id and size tier
public ParkingSpot(String id, SpotSize size) {
this.id = id;
this.size = size;
}
// True if this spot is large enough for the vehicle type
public boolean canFit(Vehicle vehicle) {
SpotSize need = VehicleSizeMapper.requiredSpot(vehicle.getType());
return size.ordinal() >= need.ordinal();
}
// Atomically parks vehicle; false if occupied or size mismatch
public synchronized boolean park(Vehicle vehicle) {
if (status == SpotStatus.OCCUPIED || !canFit(vehicle)) return false;
this.parkedVehicle = vehicle;
this.status = SpotStatus.OCCUPIED;
return true;
}
// Frees spot and returns parked vehicle; empty if already free
public synchronized Optional<Vehicle> unpark() {
if (status == SpotStatus.FREE) return Optional.empty();
Vehicle v = parkedVehicle;
parkedVehicle = null;
status = SpotStatus.FREE;
return Optional.of(v);
}
// True when spot can accept a new park request
public boolean isFree() { return status == SpotStatus.FREE; }
// Returns spot id for tickets and display boards
public String getId() { return id; }
}
public class Floor {
private final int level;
private final List<ParkingSpot> spots = new ArrayList<>();
// Creates a floor at the given level number
public Floor(int level) { this.level = level; }
// Registers a spot on this floor
public void addSpot(ParkingSpot spot) { spots.add(spot); }
// Returns first free spot on this floor that fits the vehicle
public Optional<ParkingSpot> findAvailableSpot(Vehicle vehicle) {
return spots.stream()
.filter(ParkingSpot::isFree)
.filter(s -> s.canFit(vehicle))
.findFirst();
}
// Counts free spots on this floor (for availability board)
public long freeCount() {
return spots.stream().filter(ParkingSpot::isFree).count();
}
// Read-only view of all spots on the floor
public List<ParkingSpot> getSpots() { return Collections.unmodifiableList(spots); }
}Full solution — ticket, pricing & ParkingLot
public class Ticket {
private final String ticketId;
private final Vehicle vehicle;
private final ParkingSpot spot;
private final Instant entryTime;
// Issued on park: binds vehicle, spot, and entry time for fee on exit
public Ticket(Vehicle vehicle, ParkingSpot spot) {
this.ticketId = UUID.randomUUID().toString();
this.vehicle = vehicle;
this.spot = spot;
this.entryTime = Instant.now();
}
// Id shown to user at gate for unpark
public String getTicketId() { return ticketId; }
// Used by pricing strategy to compute duration
public Instant getEntryTime() { return entryTime; }
// Spot to free when vehicle leaves
public ParkingSpot getSpot() { return spot; }
// Vehicle type drives hourly rate lookup
public Vehicle getVehicle() { return vehicle; }
}
public interface PricingStrategy {
// Computes exit fee from ticket and exit timestamp
BigDecimal calculateFee(Ticket ticket, Instant exitTime);
}
public class HourlyPricingStrategy implements PricingStrategy {
private final Map<VehicleType, BigDecimal> hourlyRate;
// Loads per-vehicle-type hourly rates
public HourlyPricingStrategy(Map<VehicleType, BigDecimal> hourlyRate) {
this.hourlyRate = hourlyRate;
}
// Bills at least one hour; rate chosen by vehicle type on ticket
@Override
public BigDecimal calculateFee(Ticket ticket, Instant exitTime) {
long minutes = Duration.between(ticket.getEntryTime(), exitTime).toMinutes();
long hours = Math.max(1, (minutes + 59) / 60);
BigDecimal rate = hourlyRate.get(ticket.getVehicle().getType());
return rate.multiply(BigDecimal.valueOf(hours));
}
}public class ParkingLot {
private final String name;
private final List<Floor> floors = new ArrayList<>();
private final PricingStrategy pricingStrategy;
private final Map<String, Ticket> activeTickets = new ConcurrentHashMap<>();
// Builds lot with name and injectable pricing strategy
public ParkingLot(String name, PricingStrategy pricingStrategy) {
this.name = name;
this.pricingStrategy = pricingStrategy;
}
// Adds a floor to the lot structure
public void addFloor(Floor floor) { floors.add(floor); }
// Finds compatible spot, parks vehicle, returns ticket; empty if full
public synchronized Optional<Ticket> parkVehicle(Vehicle vehicle) {
for (Floor floor : floors) {
Optional<ParkingSpot> spot = floor.findAvailableSpot(vehicle);
if (spot.isPresent() && spot.get().park(vehicle)) {
Ticket ticket = new Ticket(vehicle, spot.get());
activeTickets.put(ticket.getTicketId(), ticket);
return Optional.of(ticket);
}
}
return Optional.empty();
}
// Frees spot, removes ticket, returns fee; empty if invalid ticket id
public synchronized Optional<BigDecimal> unparkVehicle(String ticketId) {
Ticket ticket = activeTickets.remove(ticketId);
if (ticket == null) return Optional.empty();
ticket.getSpot().unpark();
Instant exit = Instant.now();
return Optional.of(pricingStrategy.calculateFee(ticket, exit));
}
// Counts free spots that can fit the given vehicle type (display board)
public int availableSpots(VehicleType type) {
Vehicle probe = new Vehicle("PROBE", type);
return (int) floors.stream()
.flatMap(f -> f.getSpots().stream())
.filter(ParkingSpot::isFree)
.filter(s -> s.canFit(probe))
.count();
}
}public class VehicleFactory {
// Central place to construct vehicles (extend for new types later)
public static Vehicle create(VehicleType type, String plate) {
return new Vehicle(plate, type);
}
}
public class ParkingLotDemo {
// Wires floors, spots, park, and unpark with fee printout
public static void main(String[] args) {
Map<VehicleType, BigDecimal> rates = Map.of(
VehicleType.BIKE, new BigDecimal("20"),
VehicleType.CAR, new BigDecimal("40"),
VehicleType.TRUCK, new BigDecimal("80")
);
ParkingLot lot = new ParkingLot("City Mall", new HourlyPricingStrategy(rates));
Floor f1 = new Floor(1);
f1.addSpot(new ParkingSpot("F1-S1", SpotSize.SMALL));
f1.addSpot(new ParkingSpot("F1-M1", SpotSize.MEDIUM));
f1.addSpot(new ParkingSpot("F1-L1", SpotSize.LARGE));
lot.addFloor(f1);
Vehicle car = VehicleFactory.create(VehicleType.CAR, "KA-01-AB-1234");
Optional<Ticket> ticket = lot.parkVehicle(car);
ticket.ifPresent(t -> {
System.out.println("Parked. Ticket: " + t.getTicketId());
lot.unparkVehicle(t.getTicketId())
.ifPresent(fee -> System.out.println("Fee: ₹" + fee));
});
}
}While you code — questions interviewers ask
Expect these mid-coding — answer briefly, then keep typing.
Extensions & follow-ups (senior loop)
- Display board:
AvailabilityPublisherafter each park/unpark (Observer). - Multiple entrances:
Gateid on ticket; still oneParkingLotbrain. - Electric vehicles:
EVSpotsubclass or decorator with charging fee. - Handicap / compact: enum flags on
ParkingSpot. - Distributed lot: shard by
floorId, Redis lock on spot id — only if interviewer converts to HLD.
Common mistakes (fail signals)
- Jumping to code without clarifying vehicle types and pricing.
- God class
ParkingLotwith 400 lines and noFloor/Spotseparation. - Hard-coded
if (type == CAR) fee = 40instead of Strategy. - Allowing any vehicle into any spot regardless of size.
- No ticket id — unpark by license plate only (ambiguous for two entries).
- Forgetting empty lot and invalid ticket paths.