Building a Multi-Floor Parking Lot System

Manage vehicle parking, ticket issuance, fee calculation, and occupancy tracking with PostgreSQL

By SysAdmin Β· Published 2026-05-27

Building a Multi-Floor Parking Lot System

The parking lot is perhaps the most iconic LLD interview question. But with trucks needing consecutive spots, duration-based fees, and concurrent entry/exit β€” it's harder than it looks.

1. The Problem

Design a multi-floor parking lot that supports three vehicle types:

The system must:

2. The NaΓ―ve Approach (and Why It Fails)

boolean[][] spots = new boolean[floors][spotsPerFloor];

public String parkVehicle(String vehicleNumber, String type) {
    for (int f = 0; f < floors; f++) {
        for (int s = 0; s < spotsPerFloor; s++) {
            if (!spots[f][s]) {
                spots[f][s] = true;
                return generateTicket();
            }
        }
    }
    return null;
}

Problems:

  1. Not persistent β€” restart the app, lose all parked vehicles
  2. Not concurrent β€” two threads find the same empty spot, both assign it
  3. No truck support β€” trucks need 2 consecutive spots; this doesn't check adjacency
  4. No fee tracking β€” no entry timestamp, no vehicle type association

3. The Right Model

Three PostgreSQL tables:

CREATE TABLE parking_spots (
    id SERIAL PRIMARY KEY,
    floor_num INT NOT NULL,
    spot_number INT NOT NULL,
    is_occupied BOOLEAN DEFAULT FALSE,
    UNIQUE(floor_num, spot_number)
);

CREATE TABLE parking_tickets (
    ticket_id VARCHAR(64) PRIMARY KEY,
    vehicle_number VARCHAR(64) NOT NULL,
    vehicle_type VARCHAR(16) NOT NULL,
    floor_num INT NOT NULL,
    spot_number INT NOT NULL,
    spot_count INT DEFAULT 1,
    entry_time BIGINT NOT NULL,
    exit_time BIGINT,
    fee DOUBLE PRECISION
);

CREATE TABLE parking_fee_rates (
    vehicle_type VARCHAR(16) PRIMARY KEY,
    rate_per_hour DOUBLE PRECISION NOT NULL
);

4. The Implementation, Walked Through

The Interface

public interface ParkingLotContract {
    void initialize(int floors, int spotsPerFloor);
    String parkVehicle(String vehicleNumber, String vehicleType);
    double unparkVehicle(String ticketId);
    Map<String, Object> getVehicleLocation(String ticketId);
    int getAvailableSpots(int floor, String vehicleType);
    int getTotalAvailableSpots();
    boolean isFull(String vehicleType);
    double getFeeRate(String vehicleType);
    void setFeeRate(String vehicleType, double ratePerHour);
    List<Map<String, Object>> getVehicleHistory(String vehicleNumber);
    Map<String, Object> getLotStats();
}

Parking: Finding the Right Spot

For cars and motorcycles, find the first free spot on the lowest floor:

SELECT floor_num, spot_number FROM parking_spots
WHERE is_occupied = FALSE
ORDER BY floor_num, spot_number
LIMIT 1
FOR UPDATE

The FOR UPDATE clause is critical β€” it locks the row so no other transaction can assign the same spot.

For trucks, we need two consecutive free spots:

SELECT a.floor_num, a.spot_number
FROM parking_spots a
JOIN parking_spots b ON a.floor_num = b.floor_num AND b.spot_number = a.spot_number + 1
WHERE a.is_occupied = FALSE AND b.is_occupied = FALSE
ORDER BY a.floor_num, a.spot_number
LIMIT 1
FOR UPDATE

⚠️ Trap: The self-join on spot_number = a.spot_number + 1 ensures adjacency. Without it, you might assign spots 3 and 7 to a truck β€” which makes no physical sense.

Fee Calculation

public double unparkVehicle(String ticketId) {
    long entryTime = ticket.get("entry_time");
    long now = System.currentTimeMillis();
    double hours = Math.ceil((now - entryTime) / 3_600_000.0);
    double rate = getFeeRate(vehicleType);
    double fee = hours * rate;
    
    // Mark spots as free
    db.execute("UPDATE parking_spots SET is_occupied = FALSE WHERE ...");
    // Record exit
    db.execute("UPDATE parking_tickets SET exit_time = ?, fee = ? WHERE ticket_id = ?", now, fee, ticketId);
    return fee;
}

πŸ’‘ Tip: Math.ceil() means parking for 1 minute costs 1 full hour. This is standard β€” most real parking garages charge by the hour rounded up.

Available Spots for Trucks

getAvailableSpots(floor, "TRUCK") doesn't return free spots β€” it returns free pairs of consecutive spots:

SELECT COUNT(*) FROM parking_spots a
JOIN parking_spots b ON a.floor_num = b.floor_num AND b.spot_number = a.spot_number + 1
WHERE a.floor_num = ? AND a.is_occupied = FALSE AND b.is_occupied = FALSE

Lot Statistics

public Map<String, Object> getLotStats() {
    int total = db.queryInt("SELECT COUNT(*) FROM parking_spots");
    int occupied = db.queryInt("SELECT COUNT(*) FROM parking_spots WHERE is_occupied = TRUE");
    double percent = total > 0 ? (occupied * 100.0 / total) : 0.0;
    
    // Per-type breakdown from active tickets
    Map<String, Map<String, Object>> byType = new HashMap<>();
    for (String type : List.of("CAR", "MOTORCYCLE", "TRUCK")) {
        int occ = db.queryInt("SELECT COALESCE(SUM(spot_count), 0) FROM parking_tickets WHERE vehicle_type = ? AND exit_time IS NULL", type);
        byType.put(type, Map.of("occupied", occ, "available", total - occupied));
    }
    return Map.of("totalSpots", total, "occupiedSpots", occupied, "occupancyPercent", percent, "byType", byType);
}

5. Performance + Concurrency

Row-Level Locking

SELECT ... FOR UPDATE is the key to concurrent safety. When two cars try to park simultaneously:

  1. Thread A locks spot (1,1)
  2. Thread B tries to lock spot (1,1) β€” blocks until Thread A commits
  3. Thread A commits, spot (1,1) now occupied
  4. Thread B's lock succeeds, sees spot is occupied, moves to spot (1,2)

No double-booking possible.

Preventing Double-Parking

Before parking, check if the vehicle is already parked:

SELECT 1 FROM parking_tickets
WHERE vehicle_number = ? AND exit_time IS NULL

If a row exists, the vehicle is still parked β€” reject the new park request.

6. What the Grader Checks

TestWhat It Verifies
testParkAndUnparkFull cycle: park β†’ get location β†’ unpark β†’ fee > 0
testTruckConsecutiveSpotsTrucks allocated 2 adjacent spots
testLotFullReturns null when no spots available
testNoDuplicateParkingSame vehicle can't park twice
testFeeCalculationceil(hours) Γ— rate
testAvailableSpotsCorrect count per floor/type
testVehicleHistoryFull history with entry/exit times
testConcurrentParkingNo double-booking under thread pressure
testLotStatsCorrect totalSpots, occupiedSpots, occupancyPercent

7. Takeaways

  1. Row-level locking prevents double-booking. SELECT FOR UPDATE is the standard pattern for reservation systems β€” parking lots, flight seats, hotel rooms all use it.
  1. Trucks are the interesting constraint. The consecutive-spots requirement turns a simple "find first free" into a self-join adjacency query. This is the kind of constraint that separates a toy solution from a real one.
  1. Schema design drives query simplicity. With is_occupied as a boolean on the spots table, all availability queries are simple WHERE filters. No need for a separate "availability" table.

πŸ‘‰ Try it yourself: Parking Lot System on Cruscible