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:
- Motorcycle β 1 spot
- Car β 1 spot
- Truck β 2 consecutive spots on the same floor
The system must:
- Issue unique tickets on entry
- Calculate fees based on
ceil(duration_hours) Γ hourly_rate - Track occupancy per floor and vehicle type
- Prevent double-booking under concurrent access
- Maintain vehicle parking history
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:
- Not persistent β restart the app, lose all parked vehicles
- Not concurrent β two threads find the same empty spot, both assign it
- No truck support β trucks need 2 consecutive spots; this doesn't check adjacency
- 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 + 1ensures 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:
- Thread A locks spot (1,1)
- Thread B tries to lock spot (1,1) β blocks until Thread A commits
- Thread A commits, spot (1,1) now occupied
- 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
| Test | What It Verifies |
|---|---|
testParkAndUnpark | Full cycle: park β get location β unpark β fee > 0 |
testTruckConsecutiveSpots | Trucks allocated 2 adjacent spots |
testLotFull | Returns null when no spots available |
testNoDuplicateParking | Same vehicle can't park twice |
testFeeCalculation | ceil(hours) Γ rate |
testAvailableSpots | Correct count per floor/type |
testVehicleHistory | Full history with entry/exit times |
testConcurrentParking | No double-booking under thread pressure |
testLotStats | Correct totalSpots, occupiedSpots, occupancyPercent |
7. Takeaways
- Row-level locking prevents double-booking.
SELECT FOR UPDATEis the standard pattern for reservation systems β parking lots, flight seats, hotel rooms all use it.
- 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.
- Schema design drives query simplicity. With
is_occupiedas 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