LLD Question 01
Parking Lot
Design a parking lot with floors, typed spots, vehicle allocation, ticketing, and fee calculation.
Learning note
Separate lot orchestration, spot matching, and fee calculation so each part can evolve independently.
Problem Statement
Design a parking lot system that can park different vehicle types across multiple floors and generate tickets for active parking sessions.
Functional Requirements
- The parking lot can have multiple floors.
- Each floor can have small, medium, and large spots.
- Bikes, cars, and trucks should be parked only in compatible spots.
- A ticket should be generated when a vehicle is parked.
- A fee should be calculated when a vehicle exits.
- The system should show current floor-level availability.
Core Entities
| Entity | Responsibility |
|---|---|
ParkingLot | Main orchestrator for entry, exit, floors, tickets, and fees. |
ParkingFloor | Owns spots and finds the first compatible available spot. |
ParkingSpot | Base abstraction for small, medium, and large spots. |
Vehicle | Base abstraction for bike, car, and truck. |
Ticket | Stores vehicle, spot, floor, entry time, and exit time. |
FeeCalculator | Strategy interface for pluggable fee calculation. |
Design Decisions
Singleton for Parking Lot
The current design uses a singleton because one application instance represents one parking lot. It avoids accidental duplicate lot objects while the demo runs.
Strategy for Fee Calculation
ParkingLot depends on the FeeCalculator interface instead of hard-coding fee rules. This allows hourly, flat-rate, weekend, or surge pricing later without changing parking or exit flows.
Spot Compatibility
Spot matching is pushed into spot classes via canFitVehicle(vehicle). That keeps ParkingFloor simple: it only asks each spot whether it can fit the vehicle.
Main Flow
- Create a parking lot.
- Add one or more floors.
- For each vehicle, scan floors for an available compatible spot.
- Park the vehicle and generate a ticket.
- On exit, mark the ticket exit time, release the spot, calculate fee, and remove the active ticket.
Complete Code: Bottom-Up
Read this section from the small building blocks first and then move upward. The implementation ends at ParkingLot, where parking, tickets, floors, spots, vehicles, and fees converge, followed by Main as the demo entry point.
1. Vehicle.java
public abstract class Vehicle {
protected String licensePlate;
protected VehicleType vehicleType;
public Vehicle(String licensePlate, VehicleType vehicleType){
this.licensePlate = licensePlate;
this.vehicleType = vehicleType;
}
public String getLicensePlate(){
return licensePlate;
}
public VehicleType getVehicleType(){
return vehicleType;
}
}
enum VehicleType{
BIKE, CAR, TRUCK
}2. Bike.java
public class Bike extends Vehicle{
public Bike(String licensePlate){
super(licensePlate, VehicleType.BIKE);
}
}3. Car.java
public class Car extends Vehicle{
public Car(String licensePlate){
super(licensePlate, VehicleType.CAR);
}
}4. Truck.java
public class Truck extends Vehicle{
public Truck(String licensePlate){
super(licensePlate, VehicleType.TRUCK);
}
}5. ParkingSpot.java
abstract class ParkingSpot {
private String spotId;
private Vehicle occupiedVehicle;
private SpotType type;
private SpotStatus status;
public ParkingSpot(String spotId, SpotType type){
this.spotId = spotId;
this.type = type;
this.status = SpotStatus.AVAILABLE;
}
abstract boolean canFitVehicle(Vehicle vehicle);
public boolean isSpotAvailable(){
return status == SpotStatus.AVAILABLE;
}
public void parkVehicle(Vehicle vehicle){
if(!isSpotAvailable()) throw new IllegalStateException("Spot is not Available!");
this.occupiedVehicle = vehicle;
this.status = SpotStatus.OCCUPIED;
}
public void removeVehicle(){
this.occupiedVehicle = null;
this.status = SpotStatus.AVAILABLE;
}
public String getSpotId() {
return spotId;
}
public Vehicle getOccupiedVehicle() {
return occupiedVehicle;
}
public SpotType getType() {
return type;
}
}
enum SpotType{
SMALL, MEDIUM, LARGE
}
enum SpotStatus{
OCCUPIED, AVAILABLE
}6. SmallSpot.java
public class SmallSpot extends ParkingSpot{
public SmallSpot(String spotId){
super(spotId, SpotType.SMALL);
}
@Override
boolean canFitVehicle(Vehicle vehicle){
return vehicle.getVehicleType() == VehicleType.BIKE;
}
}7. MediumSpot.java
public class MediumSpot extends ParkingSpot{
public MediumSpot(String spotId){
super(spotId, SpotType.MEDIUM);
}
@Override
boolean canFitVehicle(Vehicle vehicle){
VehicleType type = vehicle.getVehicleType();
return type == VehicleType.BIKE || type == VehicleType.CAR;
}
}8. LargeSpot.java
public class LargeSpot extends ParkingSpot{
public LargeSpot(String spotId){
super(spotId, SpotType.LARGE);
}
@Override
boolean canFitVehicle(Vehicle vehicle){
return true;
}
}9. Ticket.java
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.UUID;
public class Ticket {
private String ticketId;
private Vehicle vehicle;
private int floorNumber;
private ParkingSpot spot;
private LocalDateTime entryTime;
private LocalDateTime exitTime;
public Ticket(Vehicle vehicle, ParkingSpot spot, int floorNumber){
this.ticketId = UUID.randomUUID().toString().substring(0,8).toUpperCase();
this.vehicle = vehicle;
this.spot = spot;
this.floorNumber = floorNumber;
this.entryTime = LocalDateTime.now();
}
public void markExit(){
this.exitTime = LocalDateTime.now();
}
public long getDurationInHours(){
LocalDateTime end = (exitTime != null) ? exitTime : LocalDateTime.now();
long minutes = Duration.between(entryTime, end).toMinutes();
return Math.max(1, (long) Math.ceil(minutes / 60.0));
}
public String getTicketId() {
return ticketId;
}
public Vehicle getVehicle() {
return vehicle;
}
public int getFloorNumber() {
return floorNumber;
}
public ParkingSpot getSpot() {
return spot;
}
public LocalDateTime getEntryTime() {
return entryTime;
}
@Override
public String toString(){
return String.format("Ticket[%s] | Vehicle: %s | Floor: %d | Spot: %s | Entry: %s",
ticketId, vehicle.getLicensePlate(), floorNumber, spot.getSpotId(), entryTime);
}
}10. FeeCalculator.java
public interface FeeCalculator {
double calculateFee(Ticket ticket);
}11. HourlyFeeCalculator.java
import java.util.Map;
public class HourlyFeeCalculator implements FeeCalculator{
private static final Map<VehicleType, Double> RATES = Map.of(
VehicleType.BIKE, 10.0,
VehicleType.CAR, 20.0,
VehicleType.TRUCK, 30.0
);
@Override
public double calculateFee(Ticket ticket){
Double rate = RATES.getOrDefault(ticket.getVehicle().getVehicleType(), 20.0);
return rate * ticket.getDurationInHours();
}
}12. ParkingFloor.java
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
public class ParkingFloor {
private int floorNum;
private List<ParkingSpot> spots;
public ParkingFloor(int floorNum, int small, int medium, int large){
this.floorNum = floorNum;
this.spots = new ArrayList<>();
for(int i=0;i<small;i++){
spots.add(new SmallSpot("F" + floorNum + "-S"+ i));
}
for(int i=0;i<medium;i++){
spots.add(new MediumSpot("F" + floorNum + "-M"+ i));
}
for(int i=0;i<large;i++){
spots.add(new LargeSpot("F" + floorNum + "-L"+ i));
}
}
public Optional<ParkingSpot> findSpot(Vehicle vehicle){
return spots.stream()
.filter(spot -> spot.isSpotAvailable() && spot.canFitVehicle(vehicle))
.findFirst();
}
public int getFloorNum(){
return floorNum;
}
public long getAvailableSpotsCount(){
return spots.stream().filter(ParkingSpot::isSpotAvailable).count();
}
public void printStatus(){
System.out.println(" Floor " + floorNum + " | Available: " + getAvailableSpotsCount() + "/" + spots.size());
}
}13. ParkingLot.java
import java.util.*;
public class ParkingLot {
private static volatile ParkingLot instance;
private String name;
private List<ParkingFloor> floors;
private Map<String, Ticket> activeTickets;
private FeeCalculator feeCalculator;
private ParkingLot(String name){
this.name = name;
this.floors = new ArrayList<>();
this.activeTickets = new HashMap<>();
this.feeCalculator = new HourlyFeeCalculator();
}
public static ParkingLot getInstance(String name){
if(instance == null){
synchronized (ParkingLot.class){
if(instance == null){
instance = new ParkingLot(name);
}
}
}
return instance;
}
public void addFloor(ParkingFloor floor){
this.floors.add(floor);
}
// Entry Flow
public Ticket parkVehicle(Vehicle vehicle){
for(ParkingFloor floor: floors){
Optional<ParkingSpot> spotOpt = floor.findSpot(vehicle);
if(spotOpt.isPresent()){
ParkingSpot spot = spotOpt.get();
spot.parkVehicle(vehicle);
Ticket ticket = new Ticket(vehicle, spot, floor.getFloorNum());
activeTickets.put(ticket.getTicketId(), ticket);
return ticket;
}
}
throw new RuntimeException("Couldn't find a spot!");
}
// Exit Flow
public double exitVehicle(String ticketId){
Ticket ticket = activeTickets.get(ticketId);
if(ticket == null)
throw new IllegalStateException("Invalid Ticket!");
ticket.markExit();
ticket.getSpot().removeVehicle();
activeTickets.remove(ticketId);
double fee = feeCalculator.calculateFee(ticket);
System.out.printf("Vehicle %s exited. Duration: %d hr(s). Fee: %.2f%n",
ticket.getVehicle().getLicensePlate(), ticket.getDurationInHours(), fee);
return fee;
}
public void displayStatus() {
System.out.println("\n" + name + " - Status:");
floors.forEach(ParkingFloor::printStatus);
System.out.println(" Active tickets: " + activeTickets.size() + "\n");
}
public void setFeeCalculator(FeeCalculator calculator) {
this.feeCalculator = calculator;
}
}14. Main.java
public class Main {
public static void main(String[] args) {
// Setup parking lot
ParkingLot lot = ParkingLot.getInstance("HiTech City Parking");
lot.addFloor(new ParkingFloor(1, 5, 10, 3)); // Floor 1: 5 small, 10 medium, 3 large
lot.addFloor(new ParkingFloor(2, 5, 10, 3)); // Floor 2: same layout
lot.displayStatus();
// Park vehicles
Vehicle bike1 = new Bike("TS-BIKE-001");
Vehicle car1 = new Car("TS-CAR-042");
Vehicle car2 = new Car("TS-CAR-099");
Vehicle truck1 = new Truck("TS-TRUCK-007");
Ticket t1 = lot.parkVehicle(bike1);
Ticket t2 = lot.parkVehicle(car1);
Ticket t3 = lot.parkVehicle(car2);
Ticket t4 = lot.parkVehicle(truck1);
lot.displayStatus();
// Exit vehicles
lot.exitVehicle(t2.getTicketId());
lot.exitVehicle(t4.getTicketId());
lot.displayStatus();
}
}What I Learned
- Keep the orchestration logic in
ParkingLot, not inside the entities. - Use abstractions for rules that may change, like pricing.
- Spot selection can start simple with first-fit and later become a strategy.
- Ticket lifecycle is important because it connects parking, exit, and billing.
Possible Improvements
- Add a
SpotAllocationStrategyfor nearest-first or floor-priority allocation. - Add gates, display boards, and payment status.
- Make singleton creation depend on a lot ID if multiple parking lots are needed later.
- Add unit tests for compatibility, ticket duration, and fee calculation.