Skip to main content

Parking Lot

A classic machine-coding prompt: design the classes behind a parking lot that assigns spots to vehicles and charges a fee on the way out. The interesting decisions are not the happy path - they're what you split into its own class, and why.

Requirements

Functional

  • A vehicle enters, is assigned an available spot, and receives a ticket.
  • A vehicle exits by presenting its ticket; the spot is freed and a fee is charged for the time parked.
  • The lot has multiple floors, each with a fixed number of spots.
  • Spots come in different sizes (motorcycle, compact, large) and a vehicle can only park in a spot at least as large as itself.

Non-functional

  • Spot lookup should scale with lot size - no linear scan of every spot on every request once the lot has thousands of them (a real answer buckets spots by floor and size; this page keeps the lookup simple and calls out the upgrade in the design decisions below).
  • The fee formula changes often (flat rate today, size-based or time-of-day pricing tomorrow) and should never require touching ParkingLot.

Design

ParkingLot is the entry point but owns almost no logic itself - it delegates spot lookup to ParkingFloor, delegates fee math to a pluggable FeeStrategy, and only coordinates the two. That split is the whole design: every class answers exactly one question.

GateParkingLotParkingFloorParkingSpotFeeStrategyparkVehicle(car)1findAvailableSpot(size)2isAvailable()3assign(car)4new Ticket(car, spot)5unparkVehicle(ticket)6release()7calculateFee(ticket)8
  1. 1A vehicle arrives at the entry gate; the gate only knows the lot, never a spot.
  2. 2The lot asks each floor in turn - it never looks at a spot directly.
  3. 3The floor is the only class that walks its own spot list.
  4. 4Once a spot is found, the lot marks it occupied.
  5. 5A ticket is minted to carry the spot and entry time back to the caller.
  6. 6Later, the exit gate presents the ticket - not the vehicle or the spot.
  7. 7The spot is freed first, independent of how the fee is computed.
  8. 8Pricing is asked for a number, not consulted about the flow. Swap it and nothing else here changes.

Ticket is the one object that outlives a single method call - it is handed to the caller at entry and handed back at exit, and it is the only thing that has to carry both the spot and the entry time across that gap.

Class diagram

«interface»FeeStrategy+ calculateFee(t: Ticket): doubleParkingLot- floors: List<ParkingFloor>- feeStrategy: FeeStrategy+ parkVehicle(v): Ticket+ unparkVehicle(t): doubleParkingFloor- level: int- spots: List<ParkingSpot>+ findAvailableSpot(size): ParkingSpotParkingSpot- id: string- size: SpotSize- vehicle: Vehicle+ isAvailable(): bool+ assign(v)+ release()Vehicle- licensePlate: string- size: VehicleSizeTicket- vehicle: Vehicle- spot: ParkingSpot- entryTime: datetimeFlatRateFeeStrategy+ calculateFee(t: Ticket): doubleVehicleSizeFeeStrategy+ calculateFee(t: Ticket): double
implementsusescreates
ParkingLot coordinates; ParkingFloor finds a spot; FeeStrategy prices it. Swap the strategy, nothing else moves.

Code

import java.time.Duration;
import java.time.LocalDateTime;
import java.util.*;
 
enum VehicleSize { MOTORCYCLE, COMPACT, LARGE }
 
class Vehicle {
final String licensePlate;
final VehicleSize size;
 
Vehicle(String licensePlate, VehicleSize size) {
this.licensePlate = licensePlate;
this.size = size;
}
}
 
class ParkingSpot {
final String id;
final VehicleSize size;
private Vehicle vehicle;
 
ParkingSpot(String id, VehicleSize size) {
this.id = id;
this.size = size;
}
 
boolean isAvailable() {
return vehicle == null;
}
 
boolean fits(Vehicle v) {
return isAvailable() && size.ordinal() >= v.size.ordinal();
}
 
void assign(Vehicle v) {
this.vehicle = v;
}
 
void release() {
this.vehicle = null;
}
}
 
class ParkingFloor {
final int level;
private final List<ParkingSpot> spots;
 
ParkingFloor(int level, List<ParkingSpot> spots) {
this.level = level;
this.spots = spots;
}
 
Optional<ParkingSpot> findAvailableSpot(Vehicle v) {
return spots.stream().filter(s -> s.fits(v)).findFirst();
}
}
 
class Ticket {
final Vehicle vehicle;
final ParkingSpot spot;
final LocalDateTime entryTime;
LocalDateTime exitTime;
 
Ticket(Vehicle vehicle, ParkingSpot spot) {
this.vehicle = vehicle;
this.spot = spot;
this.entryTime = LocalDateTime.now();
}
}
 
interface FeeStrategy {
double calculateFee(Ticket ticket);
}
 
class FlatRateFeeStrategy implements FeeStrategy {
private final double ratePerHour;
 
FlatRateFeeStrategy(double ratePerHour) {
this.ratePerHour = ratePerHour;
}
 
public double calculateFee(Ticket ticket) {
long minutes = Duration.between(ticket.entryTime, ticket.exitTime).toMinutes();
return Math.ceil(minutes / 60.0) * ratePerHour;
}
}
 
class VehicleSizeFeeStrategy implements FeeStrategy {
private final Map<VehicleSize, Double> ratePerHour;
 
VehicleSizeFeeStrategy(Map<VehicleSize, Double> ratePerHour) {
this.ratePerHour = ratePerHour;
}
 
public double calculateFee(Ticket ticket) {
long minutes = Duration.between(ticket.entryTime, ticket.exitTime).toMinutes();
double rate = ratePerHour.get(ticket.vehicle.size);
return Math.ceil(minutes / 60.0) * rate;
}
}
 
class ParkingLot {
private final List<ParkingFloor> floors;
private final FeeStrategy feeStrategy;
private final Map<String, Ticket> activeTickets = new HashMap<>();
 
ParkingLot(List<ParkingFloor> floors, FeeStrategy feeStrategy) {
this.floors = floors;
this.feeStrategy = feeStrategy;
}
 
Ticket parkVehicle(Vehicle vehicle) {
for (ParkingFloor floor : floors) {
Optional<ParkingSpot> spot = floor.findAvailableSpot(vehicle);
if (spot.isPresent()) {
spot.get().assign(vehicle);
Ticket ticket = new Ticket(vehicle, spot.get());
activeTickets.put(vehicle.licensePlate, ticket);
return ticket;
}
}
throw new IllegalStateException("Lot is full for size " + vehicle.size);
}
 
double unparkVehicle(Ticket ticket) {
ticket.exitTime = LocalDateTime.now();
ticket.spot.release();
activeTickets.remove(ticket.vehicle.licensePlate);
return feeStrategy.calculateFee(ticket);
}
}

Design decisions

  • Spots split by size instead of one generic ParkingSpot. A single spot type with a size field (rather than a CompactSpot/LargeSpot class hierarchy) keeps findAvailableSpot a single comparison instead of a type check. The size differences here are data, not behavior, so a class hierarchy would be one class per enum value for no extra polymorphism gained.
  • Fee calculation is pulled into its own FeeStrategy interface. Pricing is the part of a parking lot that actually changes in the real world - promotions, size-based rates, time-of-day surcharges. Isolating it behind one method means ParkingLot never changes when pricing does; that's the Strategy pattern earning its keep rather than being applied for its own sake.
  • ParkingFloor owns its spots, ParkingLot owns its floors - composition all the way down. Nothing outside the lot ever holds a spot reference directly; every access goes through parkVehicle/unparkVehicle, so the invariant "a spot's occupant matches some active ticket" can only be broken inside this file.
  • What's missing for a real system: findAvailableSpot's linear scan needs an index (e.g. a free-list per floor per size) once a lot has more than a few hundred spots, and two attendants assigning the same spot at once needs the assignment step to be atomic (a lock per spot, or a compare-and-swap on its state) - neither is in scope for a 45-minute whiteboard version, but naming them is what separates a design that merely compiles from one that would survive contact with concurrency.
0%0 of 122 pages studied