Design a Parking Lot System
A classic object-oriented design problem covering classes, relationships, and extensibility.
1Problem Statement
- Handle multiple floors with different spot sizes
- Support different vehicle types (motorcycle, car, bus/truck)
- Track available spots and assign vehicles efficiently
- Calculate parking fees based on duration
- Handle entry/exit with ticket generation
2Requirements
Functional Requirements
- ✓Park and unpark vehicles
- ✓Track spot availability per floor
- ✓Different spot sizes for different vehicles
- ✓Generate parking tickets on entry
- ✓Calculate fees on exit
- ✓Display available spots count
Non-Functional Requirements
- ✓Handle concurrent entry/exit
- ✓Efficient spot allocation
- ✓Extensible for new vehicle types
- ✓Support multiple payment methods
- ✓Real-time availability updates
Clarifying Questions
A: 5 floors, 100 spots per floor (500 total)
A: Motorcycle, Car, Bus/Truck - each needs different spot sizes
A: Hourly rate, different for each vehicle type
A: Yes, one entry and one exit per floor
3Core Entities
| Entity | Key Attributes | Relationships |
|---|---|---|
| ParkingLot | name, floors[], entryPanels[], exitPanels[] | 1-to-Many Floors |
| ParkingFloor | floorNumber, spots[], displayBoard | 1-to-Many ParkingSpots |
| ParkingSpot | spotNumber, spotType, vehicle, isAvailable | Many-to-1 Floor, 0-1 Vehicle |
| Vehicle | licensePlate, vehicleType, color | 0-1 ParkingSpot |
| ParkingTicket | ticketNumber, entryTime, exitTime, vehicle, spot | 1-to-1 Vehicle, 1-to-1 Spot |
| Payment | amount, paymentTime, method, status | 1-to-1 Ticket |
4Class Diagram
Domain Model
┌────────────────────────────────────────────┐
│ ParkingLot (Singleton) │
├────────────────────────────────────────────┤
│ - instance: ParkingLot │
│ - name: String │
│ - floors: List<ParkingFloor> │
│ - tickets: Map<String, ParkingTicket> │
├────────────────────────────────────────────┤
│ + getInstance(): ParkingLot │
│ + addFloor(floor): void │
│ + parkVehicle(vehicle): ParkingTicket │
│ + unparkVehicle(ticket): Payment │
│ + getAvailableSpots(type): int │
└──────────────────┬─────────────────────────┘
│
│ has many
▼
┌────────────────────────────────────────────┐
│ ParkingFloor │
├────────────────────────────────────────────┤
│ - floorNumber: int │
│ - spots: Map<SpotType, List<ParkingSpot>> │
├────────────────────────────────────────────┤
│ + getAvailableSpots(type): int │
│ + findAvailableSpot(vehicleType): Spot │
│ + updateDisplayBoard(): void │
└──────────────────┬─────────────────────────┘
│
│ has many
▼
┌────────────────────────────────────────────┐
│ <<abstract>> ParkingSpot │
├────────────────────────────────────────────┤
│ - spotNumber: String │
│ - isAvailable: boolean │
│ - vehicle: Vehicle │
├────────────────────────────────────────────┤
│ + assignVehicle(v): boolean │
│ + removeVehicle(): Vehicle │
│ + canFitVehicle(v): boolean │
└────────────────────────────────────────────┘
▲ ▲ ▲
┌──────────┘ │ └──────────┐
│ │ │
┌────────┴───────┐ ┌────────────┴────────┐ ┌──────────┴───────┐
│ MotorcycleSpot │ │ CompactSpot │ │ LargeSpot │
└────────────────┘ └─────────────────────┘ └──────────────────┘
Vehicle Hierarchy
┌────────────────────────────────────────────┐
│ <<abstract>> Vehicle │
├────────────────────────────────────────────┤
│ - licensePlate: String │
│ - vehicleType: VehicleType │
│ - color: String │
├────────────────────────────────────────────┤
│ + getType(): VehicleType │
└────────────────────────────────────────────┘
▲ ▲ ▲
┌──────────┘ │ └──────────┐
│ │ │
┌────────┴───────┐ ┌────────────┴────────┐ ┌──────────┴───────┐
│ Motorcycle │ │ Car │ │ Bus/Truck │
└────────────────┘ └─────────────────────┘ └──────────────────┘
Strategy Pattern for Pricing
┌──────────────────────────────────────────┐
│ <<interface>> PricingStrategy │
├──────────────────────────────────────────┤
│ + calculateFee(ticket): double │
└──────────────────────────────────────────┘
▲
┌─────────────────┼─────────────────┐
│ │ │
┌──────┴──────┐ ┌──────┴──────┐ ┌──────┴──────┐
│HourlyPricing│ │ FlatPricing │ │WeekendPricing│
├─────────────┤ ├─────────────┤ ├─────────────┤
│ rate/hour │ │ fixed rate │ │ special rate│
└─────────────┘ └─────────────┘ └─────────────┘
5Key Flows
1. Vehicle Entry Flow
2. Vehicle Exit Flow
3. Find Available Spot Algorithm
6Algorithms
Finding Available Spot
Strategy: Find smallest spot that fits, on lowest floor
Steps:
- Get spot priority list based on vehicle type
- Iterate through floors from bottom to top
- For each floor, check spot types in priority order
- Return first available spot found
- If no spot found on any floor, return null (lot full)
Complexity: O(floors × spotTypes) = O(F × S)
Optimization: Maintain count of available spots per type per floor
Fee Calculation
Strategy Pattern: Different pricing strategies for flexibility
Hourly Pricing:
- Calculate duration = exitTime - entryTime
- Convert to hours (round up partial hours)
- Get hourly rate based on vehicle type
- Fee = hours × hourlyRate
Sample Rates:
7Design Patterns
Singleton Pattern
Where: ParkingLot class
Why: Only one instance should manage all floors and spots. Centralized resource management.
Factory Pattern
Where: Creating vehicles and spots
Why: Encapsulates object creation logic. Easy to add new vehicle/spot types.
Strategy Pattern
Where: PricingStrategy for fee calculation
Why: Allows switching pricing models without changing core logic.
Observer Pattern
Where: DisplayBoard updates
Why: Decouples spot changes from display updates. Real-time notifications.
Abstract Factory
Where: Creating related objects
Why: Create families of related objects (spots and their displays).
State Pattern
Where: Ticket status management
Why: Handle ticket state transitions (ACTIVE → PAID → EXITED).
8Enums
VehicleType
SpotType
TicketStatus
PaymentStatus
9Java Implementation
public enum VehicleType {
MOTORCYCLE, CAR, BUS, TRUCK
}
public enum SpotType {
MOTORCYCLE, COMPACT, LARGE, HANDICAPPED, ELECTRIC
}
public enum TicketStatus {
ACTIVE, PAID, EXITED, LOST
}public abstract class Vehicle {
private String licensePlate;
private VehicleType type;
private String color;
public Vehicle(String licensePlate, VehicleType type, String color) {
this.licensePlate = licensePlate;
this.type = type;
this.color = color;
}
public VehicleType getType() { return type; }
public String getLicensePlate() { return licensePlate; }
}
public class Car extends Vehicle {
public Car(String licensePlate, String color) {
super(licensePlate, VehicleType.CAR, color);
}
}
public class Motorcycle extends Vehicle {
public Motorcycle(String licensePlate, String color) {
super(licensePlate, VehicleType.MOTORCYCLE, color);
}
}
public class Bus extends Vehicle {
public Bus(String licensePlate, String color) {
super(licensePlate, VehicleType.BUS, color);
}
}public abstract class ParkingSpot {
private String spotNumber;
private SpotType spotType;
private Vehicle vehicle;
public ParkingSpot(String spotNumber, SpotType spotType) {
this.spotNumber = spotNumber;
this.spotType = spotType;
}
public boolean isAvailable() {
return vehicle == null;
}
public boolean assignVehicle(Vehicle vehicle) {
if (!isAvailable() || !canFitVehicle(vehicle)) {
return false;
}
this.vehicle = vehicle;
return true;
}
public Vehicle removeVehicle() {
Vehicle v = this.vehicle;
this.vehicle = null;
return v;
}
public abstract boolean canFitVehicle(Vehicle vehicle);
// Getters
public String getSpotNumber() { return spotNumber; }
public SpotType getSpotType() { return spotType; }
public Vehicle getVehicle() { return vehicle; }
}
public class CompactSpot extends ParkingSpot {
public CompactSpot(String spotNumber) {
super(spotNumber, SpotType.COMPACT);
}
@Override
public boolean canFitVehicle(Vehicle vehicle) {
return vehicle.getType() == VehicleType.CAR ||
vehicle.getType() == VehicleType.MOTORCYCLE;
}
}
public class LargeSpot extends ParkingSpot {
public LargeSpot(String spotNumber) {
super(spotNumber, SpotType.LARGE);
}
@Override
public boolean canFitVehicle(Vehicle vehicle) {
return true; // Large spots can fit all vehicle types
}
}
public class MotorcycleSpot extends ParkingSpot {
public MotorcycleSpot(String spotNumber) {
super(spotNumber, SpotType.MOTORCYCLE);
}
@Override
public boolean canFitVehicle(Vehicle vehicle) {
return vehicle.getType() == VehicleType.MOTORCYCLE;
}
}public class ParkingTicket {
private String ticketNumber;
private LocalDateTime entryTime;
private LocalDateTime exitTime;
private Vehicle vehicle;
private ParkingSpot spot;
private TicketStatus status;
public ParkingTicket(String ticketNumber, Vehicle vehicle, ParkingSpot spot) {
this.ticketNumber = ticketNumber;
this.vehicle = vehicle;
this.spot = spot;
this.entryTime = LocalDateTime.now();
this.status = TicketStatus.ACTIVE;
}
public void markExited() {
this.exitTime = LocalDateTime.now();
this.status = TicketStatus.EXITED;
}
public long getDurationInHours() {
LocalDateTime end = exitTime != null ? exitTime : LocalDateTime.now();
long minutes = ChronoUnit.MINUTES.between(entryTime, end);
return (long) Math.ceil(minutes / 60.0); // Round up
}
// Getters
public String getTicketNumber() { return ticketNumber; }
public Vehicle getVehicle() { return vehicle; }
public ParkingSpot getSpot() { return spot; }
public TicketStatus getStatus() { return status; }
public LocalDateTime getEntryTime() { return entryTime; }
}public class ParkingFloor {
private int floorNumber;
private Map<SpotType, List<ParkingSpot>> spots;
public ParkingFloor(int floorNumber) {
this.floorNumber = floorNumber;
this.spots = new EnumMap<>(SpotType.class);
for (SpotType type : SpotType.values()) {
spots.put(type, new ArrayList<>());
}
}
public void addSpot(ParkingSpot spot) {
spots.get(spot.getSpotType()).add(spot);
}
public ParkingSpot findAvailableSpot(VehicleType vehicleType) {
List<SpotType> priority = getSpotPriority(vehicleType);
for (SpotType spotType : priority) {
List<ParkingSpot> spotList = spots.get(spotType);
for (ParkingSpot spot : spotList) {
if (spot.isAvailable()) {
return spot;
}
}
}
return null;
}
private List<SpotType> getSpotPriority(VehicleType vehicleType) {
switch (vehicleType) {
case MOTORCYCLE:
return Arrays.asList(SpotType.MOTORCYCLE, SpotType.COMPACT, SpotType.LARGE);
case CAR:
return Arrays.asList(SpotType.COMPACT, SpotType.LARGE);
case BUS:
case TRUCK:
return Arrays.asList(SpotType.LARGE);
default:
return Arrays.asList(SpotType.LARGE);
}
}
public int getAvailableCount(SpotType spotType) {
return (int) spots.get(spotType).stream()
.filter(ParkingSpot::isAvailable)
.count();
}
public int getFloorNumber() { return floorNumber; }
}public interface PricingStrategy {
double calculateFee(ParkingTicket ticket);
}
public class HourlyPricingStrategy implements PricingStrategy {
private Map<VehicleType, Double> hourlyRates;
public HourlyPricingStrategy() {
hourlyRates = new EnumMap<>(VehicleType.class);
hourlyRates.put(VehicleType.MOTORCYCLE, 10.0);
hourlyRates.put(VehicleType.CAR, 20.0);
hourlyRates.put(VehicleType.BUS, 50.0);
hourlyRates.put(VehicleType.TRUCK, 50.0);
}
@Override
public double calculateFee(ParkingTicket ticket) {
long hours = ticket.getDurationInHours();
VehicleType type = ticket.getVehicle().getType();
double rate = hourlyRates.getOrDefault(type, 20.0);
return hours * rate;
}
}
public class FlatRatePricingStrategy implements PricingStrategy {
private double flatRate;
public FlatRatePricingStrategy(double flatRate) {
this.flatRate = flatRate;
}
@Override
public double calculateFee(ParkingTicket ticket) {
return flatRate;
}
}public class ParkingLot {
private static ParkingLot instance;
private String name;
private List<ParkingFloor> floors;
private Map<String, ParkingTicket> tickets;
private AtomicInteger ticketCounter;
private PricingStrategy pricingStrategy;
private ParkingLot(String name) {
this.name = name;
this.floors = new ArrayList<>();
this.tickets = new ConcurrentHashMap<>();
this.ticketCounter = new AtomicInteger(0);
this.pricingStrategy = new HourlyPricingStrategy();
}
public static synchronized ParkingLot getInstance(String name) {
if (instance == null) {
instance = new ParkingLot(name);
}
return instance;
}
public static ParkingLot getInstance() {
return getInstance("Default Parking Lot");
}
public void addFloor(ParkingFloor floor) {
floors.add(floor);
}
public void setPricingStrategy(PricingStrategy strategy) {
this.pricingStrategy = strategy;
}
public ParkingTicket parkVehicle(Vehicle vehicle) {
// Find available spot
for (ParkingFloor floor : floors) {
ParkingSpot spot = floor.findAvailableSpot(vehicle.getType());
if (spot != null && spot.assignVehicle(vehicle)) {
// Create ticket
String ticketNumber = "T-" + ticketCounter.incrementAndGet();
ParkingTicket ticket = new ParkingTicket(ticketNumber, vehicle, spot);
tickets.put(ticketNumber, ticket);
return ticket;
}
}
return null; // Parking lot is full
}
public double unparkVehicle(String ticketNumber) {
ParkingTicket ticket = tickets.get(ticketNumber);
if (ticket == null) {
throw new IllegalArgumentException("Invalid ticket");
}
if (ticket.getStatus() == TicketStatus.EXITED) {
throw new IllegalStateException("Ticket already used");
}
// Mark ticket as exited
ticket.markExited();
// Free the spot
ticket.getSpot().removeVehicle();
// Calculate and return fee
return pricingStrategy.calculateFee(ticket);
}
public Map<SpotType, Integer> getAvailableSpots() {
Map<SpotType, Integer> counts = new EnumMap<>(SpotType.class);
for (SpotType type : SpotType.values()) {
counts.put(type, 0);
}
for (ParkingFloor floor : floors) {
for (SpotType type : SpotType.values()) {
int current = counts.get(type);
counts.put(type, current + floor.getAvailableCount(type));
}
}
return counts;
}
public boolean isFull() {
return getAvailableSpots().values().stream()
.allMatch(count -> count == 0);
}
}public class ParkingLotDemo {
public static void main(String[] args) {
// Get singleton instance
ParkingLot lot = ParkingLot.getInstance("Downtown Parking");
// Create floor with spots
ParkingFloor floor1 = new ParkingFloor(1);
for (int i = 1; i <= 5; i++) {
floor1.addSpot(new MotorcycleSpot("1-M" + i));
}
for (int i = 1; i <= 20; i++) {
floor1.addSpot(new CompactSpot("1-C" + i));
}
for (int i = 1; i <= 10; i++) {
floor1.addSpot(new LargeSpot("1-L" + i));
}
lot.addFloor(floor1);
// Display available spots
System.out.println("Available spots: " + lot.getAvailableSpots());
// Output: {MOTORCYCLE=5, COMPACT=20, LARGE=10, ...}
// Park a car
Vehicle car = new Car("ABC-123", "Red");
ParkingTicket ticket = lot.parkVehicle(car);
if (ticket != null) {
System.out.println("Car parked at spot: " + ticket.getSpot().getSpotNumber());
System.out.println("Ticket: " + ticket.getTicketNumber());
}
// Simulate some time passing...
// Thread.sleep(3600000); // 1 hour
// Unpark and pay
double fee = lot.unparkVehicle(ticket.getTicketNumber());
System.out.println("Parking fee: $" + fee);
// Check spots again
System.out.println("Available spots after exit: " + lot.getAvailableSpots());
}
}10Interview Follow-up Questions
Handle Concurrent Entry/Exit?
Use synchronized blocks for spot assignment, ConcurrentHashMap for tickets, AtomicInteger for ticket counter.
Electric Vehicle Support?
Add ElectricSpot with charging capability. Vehicle has 'needsCharging' flag. Prioritize electric spots for EVs.
Reservation System?
Add Reservation class with user, time slot, vehicle type. Check reservations before spot assignment.
Multi-entrance Support?
Each entrance has its own panel. All share the same ParkingLot singleton. Synchronize spot allocation.
Lost Ticket Handling?
Charge maximum daily rate. Verify vehicle via license plate scan. Manual override by attendant.
Peak Hour Pricing?
Implement TimeBasedPricingStrategy. Check current time in calculateFee(). Apply surge multiplier.
11Key Takeaways
?Quiz
1. Why use Singleton for ParkingLot?
2. A motorcycle can park in which spots?
3. What pattern is used for fee calculation?