Model the domain first
Before writing any behaviour, name the nouns. A parking lot has spots of different sizes, vehicles that fit certain spots, and tickets issued on entry. Start with small, closed value types.
enum VehicleSize { MOTORCYCLE, CAR, TRUCK }
class Vehicle {
private final String plate;
private final VehicleSize size;
Vehicle(String plate, VehicleSize size) { this.plate = plate; this.size = size; }
VehicleSize getSize() { return size; }
}
class ParkingSpot {
private final int id;
private final VehicleSize size;
private Vehicle vehicle; // null when free
ParkingSpot(int id, VehicleSize size) { this.id = id; this.size = size; }
VehicleSize size() { return size; }
boolean isFree() { return vehicle == null; }
boolean fits(Vehicle v) { return isFree() && v.getSize().ordinal() <= size.ordinal(); }
void assign(Vehicle v) { this.vehicle = v; }
void release() { this.vehicle = null; }
}
The vehicle field is private and mutated only through assign/release — no caller can leave a spot half-occupied. Using an enum for size (instead of MotorcycleSpot/CarSpot subclasses) turns "does this vehicle fit?" into a one-line ordinal comparison rather than a type check.
Make spot-finding a strategy
Which free spot should a car get? The nearest? The smallest that fits (to save big spots for trucks)? You may not know yet — and the interviewer might change it. Hide the choice behind an interface so the lot never hard-codes one policy.
interface SpotFinder {
ParkingSpot find(List<ParkingSpot> spots, Vehicle v);
}
class SmallestFitFinder implements SpotFinder {
public ParkingSpot find(List<ParkingSpot> spots, Vehicle v) {
return spots.stream()
.filter(s -> s.fits(v))
.min(Comparator.comparingInt(s -> s.size().ordinal())) // smallest that fits
.orElse(null);
}
}
Now switching to a "nearest spot" policy is a new class, not an edit to the lot — the Open/Closed Principle in one move.
<!--STEP-->Issue a ticket on entry
Parking must capture when a car entered so we can price it later. That belongs on a Ticket, created atomically with the assignment.
class Ticket {
private final String id;
private final ParkingSpot spot;
private final Instant entryAt;
Ticket(String id, ParkingSpot spot) {
this.id = id; this.spot = spot; this.entryAt = Instant.now();
}
Instant entryAt() { return entryAt; }
ParkingSpot spot() { return spot; }
}
Recording entryAt at creation — not recomputing it at exit — is the only way to price the actual stay. If you tried to derive entry time on the way out, you would have nothing to derive it from.
Price with a swappable strategy
Flat rate? Hourly? Weekend surge? Pricing changes far more often than parking mechanics. Isolate it behind its own interface.
interface PricingStrategy {
long feeCents(Ticket t, Instant exitAt);
}
class HourlyPricing implements PricingStrategy {
private final long ratePerHourCents;
HourlyPricing(long rate) { this.ratePerHourCents = rate; }
public long feeCents(Ticket t, Instant exitAt) {
long hours = Math.max(1, Duration.between(t.entryAt(), exitAt).toHours());
return hours * ratePerHourCents;
}
}
When finance asks for surge pricing on Fridays, you add SurgePricing and inject it — ParkingLot doesn't change. Same shape as SpotFinder: behaviour that varies lives behind an interface.
Assemble the lot — and guard the race
class ParkingLot {
private final List<ParkingSpot> spots;
private final SpotFinder finder;
private final PricingStrategy pricing;
ParkingLot(List<ParkingSpot> spots, SpotFinder f, PricingStrategy p) {
this.spots = spots; this.finder = f; this.pricing = p;
}
synchronized Ticket park(Vehicle v) { // guard find + assign
ParkingSpot s = finder.find(spots, v);
if (s == null) throw new IllegalStateException("Lot full for " + v.getSize());
s.assign(v);
return new Ticket(UUID.randomUUID().toString(), s);
}
long unpark(Ticket t) {
t.spot().release();
return pricing.feeCents(t, Instant.now());
}
}
park is synchronized because find-then-assign is a check-then-act: two threads could both see the same free spot and both assign it. The lock makes selection and reservation one atomic step. A real multi-entry garage would shrink the critical section (lock per spot / CAS), but the invariant is identical: never hand the same spot to two cars.
Practice it for real: the Parking Lot problem in the LLD ladder gives you a class skeleton and hidden tests that exercise exactly these flows.