Design Parking Lot

Designing a Parking Lot System is a foundational Low-Level Design problem that tests how well we model real-world systems using clean object-oriented principles. The goal is not just to "fit vehicles into slots," but to build a system that is scalable, thread-safe, and extensible for future enhancements like dynamic pricing, reservations, and analytics.

A well-designed system separates concerns clearly: domain modeling, allocation logic, pricing, and orchestration. This separation ensures that changes in one part do not ripple unnecessarily into others.

A parking lot consists of multiple floors and slots. Vehicles of different types such as bikes, cars, and trucks enter the system, are assigned a compatible slot, and exit after payment. The system must efficiently track available slots, manage parking sessions, and calculate charges based on duration.

Unlike naive designs, scanning all slots for every request is inefficient. Instead, we aim for constant-time allocation using optimized data structures and concurrent-safe mechanisms.

Domain Modeling

Vehicle

At the core of the system lies the Vehicle abstraction, which encapsulates the identity and type of a vehicle. This allows the system to enforce compatibility rules between vehicles and parking slots.
enum VehicleType {
    BIKE,
    CAR,
    TRUCK
}
abstract class Vehicle {

    private final String licenseNumber;
    private final VehicleType type;

    public Vehicle(String licenseNumber, VehicleType type) {
        this.licenseNumber = licenseNumber;
        this.type = type;
    }

    // Getters
}
This abstraction ensures that adding new vehicle types in the future does not affect existing logic.

Parking Structure

The parking lot is composed of floors, each containing multiple parking slots. Unlike simplistic designs that use enums for floors, we model floors as objects to allow scalability.

Each ParkingSlot is responsible for maintaining its own state, making it the single source of truth for occupancy.
import java.util.UUID;

class ParkingSlot {

    private final UUID id;
    private final VehicleType type;
    private final double chargePerHour;

    private boolean occupied;
    private Vehicle currentVehicle;

    public ParkingSlot(UUID id, VehicleType type, double chargePerHour) {
        this.id = id;
        this.type = type;
        this.chargePerHour = chargePerHour;
    }

    public synchronized boolean canFit(Vehicle vehicle) {
        return !occupied && vehicle.getType() == type;
    }

    public synchronized void park(Vehicle vehicle) {
        this.currentVehicle = vehicle;
        this.occupied = true;
    }

    public synchronized void free() {
        this.currentVehicle = null;
        this.occupied = false;
    }

    public VehicleType getType() {
        return type;
    }

    public double getChargePerHour() {
        return chargePerHour;
    }
}
import java.util.List;

class ParkingFloor {

    private final String id;
    private final List<ParkingSlot> slots;

    public ParkingFloor(String id, List<ParkingSlot> slots) {
        this.id = id;
        this.slots = slots;
    }

    public List<ParkingSlot> getSlots() {
        return slots;
    }
}
import java.util.List;

class ParkingLot {

    private final List<ParkingFloor> floors;

    public ParkingLot(List<ParkingFloor> floors) {
        this.floors = floors;
    }

    public List<ParkingFloor> getFloors() {
        return floors;
    }
}
This hierarchical structure mirrors the real world while remaining flexible.

Parking Session

A critical insight in this design is modeling parking as a session. The Parking class represents the lifecycle of a vehicle inside the lotβ€”from entry to exit.
import java.time.Instant;
import java.util.UUID;

class Parking {

    private final UUID id;
    private final ParkingSlot slot;
    private final Vehicle vehicle;
    private final Instant entryTime;
    private Instant exitTime;

    public Parking(UUID id, ParkingSlot slot, Vehicle vehicle) {
        this.id = id;
        this.slot = slot;
        this.vehicle = vehicle;
        this.entryTime = Instant.now();
    }

    public void markExit() {
        this.exitTime = Instant.now();
    }

    public Instant getEntryTime() {
        return entryTime;
    }

    public Instant getExitTime() {
        return exitTime;
    }

    public ParkingSlot getSlot() {
        return slot;
    }
}
The Ticket acts as a handle for this session and carries contextual information instead of being a mere identifier.
import java.time.Instant;
import java.util.UUID;

class Ticket {

    private final UUID id;
    private final Parking parking;
    private final Instant issuedAt;

    public Ticket(UUID id, Parking parking) {
        this.id = id;
        this.parking = parking;
        this.issuedAt = Instant.now();
    }

    public Parking getParking() {
        return parking;
    }
}
This approach eliminates unnecessary lookups and keeps the system cohesive.

Pricing and Payment

To ensure flexibility, pricing is implemented using the Strategy Pattern. This allows different pricing models to be plugged in without changing core logic.
import java.time.Instant;

interface PricingStrategy {

    double calculate(Instant entry, Instant exit, double ratePerHour);
}
class HourlyPricingStrategy implements PricingStrategy {

    @Override
    public double calculate(Instant entry, Instant exit, double ratePerHour) {
        long hours = Math.max(
                1,
                (exit.toEpochMilli() - entry.toEpochMilli()) / (1000 * 60 * 60)
        );
        return hours * ratePerHour;
    }
}
Payment is modeled with a lifecycle state to support real-world scenarios such as failures and retries.
import java.util.UUID;

enum PaymentStatus {
    PENDING,
    SUCCESS,
    FAILED
}
import java.util.UUID;

class Payment {

    private final UUID id;
    private final Parking parking;
    private final double amount;
    private PaymentStatus status;

    public Payment(UUID id, Parking parking, double amount) {
        this.id = id;
        this.parking = parking;
        this.amount = amount;
        this.status = PaymentStatus.PENDING;
    }

    public void markSuccess() {
        this.status = PaymentStatus.SUCCESS;
    }

    public Parking getParking() {
        return parking;
    }
}

Slot Allocation

The system performs slot allocation entirely at the database level to ensure both efficiency and correctness under concurrency. Instead of relying on in-memory structures or multiple queries, the allocation is handled using a single atomic database operation that both selects and reserves a slot in one step.

This approach eliminates the need for caching, retry loops, or synchronization logic in the application layer. The database, optimized with proper indexing, efficiently finds an available slot and guarantees that no two concurrent requests can allocate the same slot.
class SlotManager {

    private final ParkingSlotRepository repository;

    public SlotManager(ParkingSlotRepository repository) {
        this.repository = repository;
    }

    public ParkingSlot allocate(VehicleType type) {
        return repository.allocateSlotAndReturn(type);
    }

    public void free(ParkingSlot slot) {
        repository.markFree(slot.getId());
    }
}
The allocation is implemented using a single query that locks, updates, and returns a slot:
WITH cte AS (
    SELECT id
    FROM parking_slot
    WHERE type = :type
      AND occupied = false
    ORDER BY id
    FOR UPDATE SKIP LOCKED
    LIMIT 1
)
UPDATE parking_slot s
SET occupied = true
FROM cte
WHERE s.id = cte.id
RETURNING s.*;
The query uses a CTE (WITH clause) to select one available slot of the given type and locks it using FOR UPDATE SKIP LOCKED, ensuring concurrent transactions skip already locked rows instead of waiting.

It then updates that same row to mark it as occupied and uses RETURNING to fetch the allocated slot in the same query, ensuring atomic allocation without race conditions.

To keep allocation fast even at scale, a composite index is used:
 CREATE INDEX idx_slot_type_occupied ON parking_slot(type, occupied); 
This allows the database to quickly locate available slots without scanning the entire table.

System Orchestration

The ParkingService connects all components and defines the system's behavior. This is where the real workflow lives.
import java.util.UUID;

class ParkingService {

    private final SlotManager slotManager;
    private final PricingStrategy pricingStrategy;

    public ParkingService(SlotManager slotManager, PricingStrategy pricingStrategy) {
        this.slotManager = slotManager;
        this.pricingStrategy = pricingStrategy;
    }

    public Ticket park(Vehicle vehicle) {
        ParkingSlot slot = slotManager.allocate(vehicle.getType());

        if (slot == null) {
            throw new RuntimeException("No slots available");
        }

        slot.park(vehicle);

        Parking parking = new Parking(UUID.randomUUID(), slot, vehicle);

        return new Ticket(UUID.randomUUID(), parking);
    }

    public Payment exit(Ticket ticket) {
        Parking parking = ticket.getParking();

        parking.markExit();

        double amount = pricingStrategy.calculate(
                parking.getEntryTime(),
                parking.getExitTime(),
                parking.getSlot().getChargePerHour()
        );

        slotManager.free(parking.getSlot());

        Payment payment = new Payment(UUID.randomUUID(), parking, amount);

        return payment;
    }
}
This layer ensures that all operations follow a consistent and controlled flow.

Conclusion

The design achieves single responsibility by ensuring each class has a well-defined role. It avoids duplication by keeping occupancy within ParkingSlot, and it ensures scalability through efficient data structures.

Concurrency is handled using atomic database operations, and extensibility is achieved through abstractions like PricingStrategy.

Such a design not only performs well under load but also adapts gracefully to future requirements, which is the true hallmark of strong Low-Level Design.
Nagesh Chauhan
Nagesh Chauhan
Principal Engineer | Java Β· Spring Boot Β· Python Β· Microservices Β· AI/ML

Principal Engineer with 14+ years of experience in designing scalable systems using Java, Spring Boot, and Python. Specialized in microservices architecture, system design, and machine learning.

Share this Article

πŸ’¬ Comments

Join the discussion