Definition
- Vehicle – carries a
VehicleTypeenum (BIKE, CAR, TRUCK) that determines the required spot size. - ParkingSpot – has a size, a spot number, and an occupied flag; owns its own state transitions.
- ParkingFloor – holds a list of spots and provides
findNearestSpot(VehicleType). - ParkingLot – the orchestrator that coordinates floors, issues tickets at entry, and computes fees at exit.
- Ticket – records entry time, assigned spot, and vehicle reference.
- FeeStrategy – a pluggable interface for flat, hourly, or surge pricing.
Think of the parking lot as the vault of National Bank of India where lockers come in three sizes. When Rajesh arrives with a small item he gets a small locker, Priya with a large item gets a large locker, the clerk writes a slip noting the entry time, and when they leave the cashier charges by the hours used. The parking lot assigns spots exactly like the bank assigns lockers.
Understanding the Concepts
What are the core entities?
The core entities are Vehicle, ParkingSpot, ParkingFloor, ParkingLot, Ticket, and FeeStrategy. Each has a single responsibility.
package com.bank.parking.model;
public enum VehicleType {
BIKE, CAR, TRUCK
}
package com.bank.parking.model;
public class Vehicle {
private final String licensePlate;
private final VehicleType type;
public Vehicle(String licensePlate, VehicleType type) {
this.licensePlate = licensePlate;
this.type = type;
}
public String getLicensePlate() { return licensePlate; }
public VehicleType getType() { return type; }
}
How does ParkingSpot work?
A ParkingSpot has a size, a spot number, and an occupied flag. It owns its own state transitions: the spot decides how it becomes occupied or freed.
package com.bank.parking.model;
public class ParkingSpot {
private final String spotId;
private final VehicleType size;
private boolean occupied;
public ParkingSpot(String spotId, VehicleType size) {
this.spotId = spotId;
this.size = size;
this.occupied = false;
}
public boolean canFit(VehicleType vehicleType) {
return this.size.ordinal() >= vehicleType.ordinal();
}
public synchronized void occupy() {
if (occupied) throw new IllegalStateException(
"Spot " + spotId + " is already occupied");
occupied = true;
}
public synchronized void free() {
if (!occupied) throw new IllegalStateException(
"Spot " + spotId + " is already free");
occupied = false;
}
public boolean isOccupied() { return occupied; }
public String getSpotId() { return spotId; }
public VehicleType getSize() { return size; }
}
How does ParkingFloor find the nearest spot?
A ParkingFloor holds a list of spots and provides a method to find the nearest free spot that can fit a given vehicle type.
package com.bank.parking.model;
import java.util.List;
import java.util.Optional;
public class ParkingFloor {
private final String floorId;
private final List<ParkingSpot> spots;
public ParkingFloor(String floorId, List<ParkingSpot> spots) {
this.floorId = floorId;
this.spots = spots;
}
public Optional<ParkingSpot> findNearestSpot(VehicleType type) {
return spots.stream()
.filter(s -> !s.isOccupied() && s.canFit(type))
.findFirst();
}
public String getFloorId() { return floorId; }
}
For production scale, replace the linear scan with a per-size priority queue keyed by distance so lookups are logarithmic.
How does the ticket system work?
A Ticket records the entry time, the assigned spot, and the vehicle reference.
package com.bank.parking.model;
import java.time.LocalDateTime;
public class Ticket {
private final String ticketId;
private final Vehicle vehicle;
private final ParkingSpot spot;
private final LocalDateTime entryTime;
public Ticket(String ticketId, Vehicle vehicle,
ParkingSpot spot, LocalDateTime entryTime) {
this.ticketId = ticketId;
this.vehicle = vehicle;
this.spot = spot;
this.entryTime = entryTime;
}
public String getTicketId() { return ticketId; }
public Vehicle getVehicle() { return vehicle; }
public ParkingSpot getSpot() { return spot; }
public LocalDateTime getEntryTime() { return entryTime; }
}
How does payment work with a FeeStrategy?
A FeeStrategy interface lets the lot support different pricing models without editing the lot code.
package com.bank.parking.service;
import java.math.BigDecimal;
public interface FeeStrategy {
BigDecimal computeFee(long hoursParked);
}
package com.bank.parking.service;
import java.math.BigDecimal;
public class HourlyFeeStrategy implements FeeStrategy {
private final BigDecimal hourlyRate;
public HourlyFeeStrategy(BigDecimal hourlyRate) {
this.hourlyRate = hourlyRate;
}
@Override
public BigDecimal computeFee(long hoursParked) {
return hourlyRate.multiply(BigDecimal.valueOf(
Math.max(1, hoursParked)));
}
}
package com.bank.parking.service;
import java.math.BigDecimal;
public class FlatFeeStrategy implements FeeStrategy {
private final BigDecimal flatRate;
public FlatFeeStrategy(BigDecimal flatRate) {
this.flatRate = flatRate;
}
@Override
public BigDecimal computeFee(long hoursParked) {
return flatRate;
}
}
How does ParkingLot orchestrate entry and exit?
The ParkingLot ties everything together.
package com.bank.parking.service;
import com.bank.parking.model.*;
import java.math.BigDecimal;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;
public class ParkingLot {
private final List<ParkingFloor> floors;
private final FeeStrategy feeStrategy;
public ParkingLot(List<ParkingFloor> floors,
FeeStrategy feeStrategy) {
this.floors = floors;
this.feeStrategy = feeStrategy;
}
public Ticket entry(Vehicle vehicle) {
for (ParkingFloor floor : floors) {
var spot = floor.findNearestSpot(vehicle.getType());
if (spot.isPresent()) {
spot.get().occupy();
Ticket ticket = new Ticket(
UUID.randomUUID().toString(),
vehicle, spot.get(), LocalDateTime.now());
System.out.println("Assigned spot "
+ spot.get().getSpotId()
+ " on floor " + floor.getFloorId()
+ " to " + vehicle.getLicensePlate());
return ticket;
}
}
throw new IllegalStateException("No spot available for "
+ vehicle.getType());
}
public BigDecimal exit(Ticket ticket) {
ticket.getSpot().free();
long hours = Duration.between(
ticket.getEntryTime(), LocalDateTime.now()).toHours();
BigDecimal fee = feeStrategy.computeFee(hours);
System.out.println("Vehicle " + ticket.getVehicle()
.getLicensePlate() + " exited, fee: " + fee);
return fee;
}
}
How do you handle concurrency?
Two cars arriving at the same instant must never be assigned the same spot. The ParkingSpot.occupy() method is synchronised. In production, use an atomic compare-and-set on the occupied flag or a database-level lock.
// Thread-safe spot allocation
public synchronized void occupy() {
if (occupied) throw new IllegalStateException("Already occupied");
occupied = true;
}
What about extensibility?
The inevitable follow-up is "how would you add electric-vehicle charging spots?" A good design answers this by adding a new VehicleType (EV) and a spot attribute for charging capability, not by rewriting existing classes. The FeeStrategy can add a surcharge for charging, again as a new strategy class.
What should a senior engineer know?
- Replace linear spot scans with per-size priority queues for O(log n) allocation at scale.
- Cache free-spot counts per floor in Redis for display boards.
- Use the Strategy pattern for fees so pricing changes are configuration, not code changes.
- Model entry and exit as separate gate services in a distributed system, with the ticket as the shared state.
What are common interview follow-up questions?
- How would you add electric-vehicle charging spots?
- How would you implement surge pricing during peak hours?
- How do you prevent two vehicles from getting the same spot?
- What is the time complexity of finding the nearest spot?
- How would you scale this to a multi-building campus?
Visual Overview
Entry Flow:
───────────
Vehicle arrives
│
▼
ParkingLot.entry(vehicle)
│
▼
ParkingFloor.findNearestSpot(type)
│
▼
ParkingSpot.occupy() ← synchronized
│
▼
Issue Ticket(vehicle, spot, entryTime)
Exit Flow:
──────────
Ticket presented
│
▼
ParkingSpot.free()
│
▼
FeeStrategy.computeFee(hours)
│
▼
Charge and release
┌────────────────────────────────────────┐
│ ParkingLot │
│ entry(vehicle) → Ticket │
│ exit(ticket) → BigDecimal fee │
└──────┬──────────────────┬──────────────┘
│ │
▼ ▼
┌──────────────┐ ┌──────────────┐
│ ParkingFloor │ │ FeeStrategy │
│ findNearest()│ │ computeFee() │
└──────┬───────┘ └──────────────┘
│
▼
┌──────────────┐
│ ParkingSpot │
│ occupy() │
│ free() │
└──────────────┘
Key Takeaway
When you design a parking lot, model it as a small set of cohesive classes rather than one giant manager. The core entities are Vehicle with a type that captures size, ParkingSpot that carries a size and an occupied flag, ParkingFloor that owns its spots and finds the nearest free compatible spot, Ticket that records the entry time, and ParkingLot that orchestrates entry and exit.
On entry, the lot walks its floors and asks each floor to find the nearest free spot of the right size. The allocation is thread-safe because two vehicles arriving at the same instant must never receive the same spot. Once a spot is reserved, a ticket is issued stamped with the entry time.
On exit, the lot computes the parked duration and applies a fee strategy. The fee strategy is deliberately pulled into an interface so the facility can switch from flat to hourly to surge pricing by adding a new class, never touching the orchestrator. That is the Open/Closed Principle paying off directly in a feature request.
For scale, replace the linear spot scan with per-size priority queues keyed by distance so lookups stay fast even with thousands of spots, and cache free-spot counts per floor for display boards. The design maps directly to assigning vault lockers at National Bank of India, where size compatibility, safe concurrent allocation, and duration-based charging are exactly the same concerns.
Remember one sentence: a parking lot assigns the nearest compatible spot atomically, issues a ticket with entry time, and charges by duration through a pluggable fee strategy.
---