Search…
Low Level Design · Part 6

Designing a parking lot

In this series (15 parts)
  1. Introduction to low level design
  2. SOLID principles
  3. Design patterns: Creational
  4. Design patterns: Structural
  5. Design patterns: Behavioral
  6. Designing a parking lot
  7. Designing a library management system
  8. Designing an elevator system
  9. Designing a hotel booking system
  10. Designing a ride-sharing model
  11. Designing a rate limiter
  12. Designing a logging framework
  13. Designing a notification system
  14. API design and contract-first development
  15. Data modeling for system design

A parking lot seems trivial until you sit down and model it. Multiple vehicle types, multiple floors, concurrent entry and exit, payment calculation, real-time spot tracking. The domain is small enough to fit in your head yet rich enough to exercise every core OOP skill.

Requirements gathering

Before writing a single class, pin down what the system must do. This is the step most candidates rush through in an interview, and it costs them.

Functional requirements:

  1. The parking lot has multiple floors. Each floor has multiple spots.
  2. Spots come in three sizes: compact, regular, and large.
  3. Vehicles come in three types: motorcycle, car, and truck. A vehicle can only park in a spot that fits its type or larger.
  4. On entry, the system assigns the nearest available spot and issues a ticket.
  5. On exit, the system calculates the fee based on duration and collects payment.
  6. The system tracks real-time availability per floor and per spot type.

Non-functional requirements:

  • Thread-safe spot allocation (two cars arriving simultaneously must not get the same spot).
  • The design should be extensible: adding an EV charging spot type should not require rewriting existing classes.

Identifying entities

Read the requirements again and underline the nouns. You get: parking lot, floor, spot, vehicle, ticket, payment. Each noun is a candidate class. Some will become enums (vehicle type, spot type, payment status). Here is the full list:

EntityRole
ParkingLotTop-level aggregate, holds floors
FloorHolds a collection of spots
ParkingSpotIndividual spot with type and occupancy state
VehicleRepresents a parked vehicle
TicketIssued on entry, closed on exit
PaymentCalculates and records fees

Class diagram

classDiagram
  class ParkingLot {
      -String id
      -String name
      -List~Floor~ floors
      +addFloor(Floor)
      +getAvailableSpots(VehicleType) int
      +assignSpot(Vehicle) Ticket
      +processExit(Ticket) Payment
  }

  class Floor {
      -int floorNumber
      -List~ParkingSpot~ spots
      +getAvailableSpots(SpotType) List~ParkingSpot~
      +findFirstAvailable(SpotType) ParkingSpot
  }

  class ParkingSpot {
      -String spotId
      -int spotNumber
      -SpotType type
      -boolean occupied
      -Vehicle currentVehicle
      +isAvailable() boolean
      +assignVehicle(Vehicle) void
      +removeVehicle() Vehicle
  }

  class Vehicle {
      -String licensePlate
      -VehicleType type
  }

  class Ticket {
      -String ticketId
      -Vehicle vehicle
      -ParkingSpot spot
      -DateTime entryTime
      -DateTime exitTime
      -TicketStatus status
      +closeTicket(DateTime) void
  }

  class Payment {
      -String paymentId
      -Ticket ticket
      -double amount
      -PaymentMethod method
      -PaymentStatus status
      +calculate(HourlyRate) double
      +processPayment() boolean
  }

  class SpotType {
      <<enumeration>>
      COMPACT
      REGULAR
      LARGE
  }

  class VehicleType {
      <<enumeration>>
      MOTORCYCLE
      CAR
      TRUCK
  }

  class TicketStatus {
      <<enumeration>>
      ACTIVE
      PAID
      LOST
  }

  class PaymentStatus {
      <<enumeration>>
      PENDING
      COMPLETED
      FAILED
  }

  ParkingLot "1" *-- "many" Floor
  Floor "1" *-- "many" ParkingSpot
  ParkingSpot "1" o-- "0..1" Vehicle
  Ticket "1" --> "1" Vehicle
  Ticket "1" --> "1" ParkingSpot
  Payment "1" --> "1" Ticket
  ParkingSpot --> SpotType
  Vehicle --> VehicleType

Full class diagram for the parking lot system.

Mapping vehicle types to spot types

A motorcycle fits in any spot. A car fits in regular or large. A truck needs large. Encode this as a simple lookup rather than a chain of if-else blocks.

public class VehicleSpotMapper {
    private static final Map<VehicleType, List<SpotType>> ALLOWED = Map.of(
        VehicleType.MOTORCYCLE, List.of(SpotType.COMPACT, SpotType.REGULAR, SpotType.LARGE),
        VehicleType.CAR,        List.of(SpotType.REGULAR, SpotType.LARGE),
        VehicleType.TRUCK,      List.of(SpotType.LARGE)
    );

    public static List<SpotType> getAllowedSpots(VehicleType v) {
        return ALLOWED.get(v);
    }
}

This is the Strategy pattern in disguise. If pricing varies by spot type, the same table-driven approach works.

Spot allocation algorithm

Walk each floor from the bottom up. On each floor, iterate through spots of compatible types in order. Return the first available one. This gives “nearest to entrance” behavior when the entrance is on the ground floor.

public Ticket assignSpot(Vehicle vehicle) {
    List<SpotType> allowed = VehicleSpotMapper.getAllowedSpots(vehicle.getType());

    for (Floor floor : floors) {
        for (SpotType type : allowed) {
            ParkingSpot spot = floor.findFirstAvailable(type);
            if (spot != null) {
                spot.assignVehicle(vehicle);
                return new Ticket(vehicle, spot, LocalDateTime.now());
            }
        }
    }
    throw new ParkingFullException("No available spot for " + vehicle.getType());
}

Concurrency for spot allocation

Two threads calling assignSpot at the same time can both see the same spot as available, then both assign to it. Classic race condition.

Option 1: Synchronized block. Wrap the allocation loop in a synchronized block on the floor object. Simple, but serializes all allocations on a given floor.

Option 2: Atomic compare-and-set. Give each ParkingSpot an AtomicBoolean for occupancy. The assignVehicle method uses compareAndSet(false, true). If it fails, the caller moves to the next spot.

public boolean assignVehicle(Vehicle vehicle) {
    if (occupied.compareAndSet(false, true)) {
        this.currentVehicle = vehicle;
        return true;
    }
    return false;
}

Option 2 is lock-free and allows genuine parallelism across spots. It is the preferred approach for high-throughput lots.

Data model (ER diagram)

When you persist this system to a relational database, the class hierarchy flattens into tables.

erDiagram
  PARKING_LOT ||--|{ FLOOR : contains
  FLOOR ||--|{ PARKING_SPOT : contains
  PARKING_SPOT ||--o| VEHICLE : holds
  TICKET ||--|| PARKING_SPOT : references
  TICKET ||--|| VEHICLE : issuedFor
  PAYMENT ||--|| TICKET : settles

  PARKING_LOT {
      string id PK
      string name
      string address
  }
  FLOOR {
      int floor_number PK
      string lot_id FK
      int total_spots
  }
  PARKING_SPOT {
      string spot_id PK
      int floor_number FK
      string spot_type
      boolean is_occupied
  }
  VEHICLE {
      string license_plate PK
      string vehicle_type
  }
  TICKET {
      string ticket_id PK
      string license_plate FK
      string spot_id FK
      datetime entry_time
      datetime exit_time
      string status
  }
  PAYMENT {
      string payment_id PK
      string ticket_id FK
      decimal amount
      string method
      string status
  }

Entity-relationship diagram for the parking lot database schema.

Fee calculation

Keep the pricing strategy separate from the Ticket class. A FeeCalculator interface allows flat-rate, hourly, and tiered models to coexist.

public interface FeeCalculator {
    double calculate(Ticket ticket);
}

public class HourlyFeeCalculator implements FeeCalculator {
    private final Map<SpotType, Double> rates;

    public HourlyFeeCalculator(Map<SpotType, Double> rates) {
        this.rates = rates;
    }

    public double calculate(Ticket ticket) {
        long hours = ChronoUnit.HOURS.between(ticket.getEntryTime(), ticket.getExitTime());
        if (hours == 0) hours = 1; // minimum one hour
        double rate = rates.get(ticket.getSpot().getType());
        return hours * rate;
    }
}

Exit flow

  1. The driver presents the ticket (scans a barcode or enters the ticket ID).
  2. The system looks up the ticket, records the exit time, and calculates the fee.
  3. The driver pays (cash, card, or UPI).
  4. On successful payment, the spot is freed and the ticket is marked PAID.
  5. The barrier opens.

If the ticket is lost, charge the maximum daily rate and mark the ticket LOST.

Extensibility checklist

ChangeImpact
Add EV charging spotsNew SpotType enum value, new entry in mapper
Add valet parkingNew ValetService class, Ticket gains a flag
Multi-entry/multi-exit gatesEntry/exit gate classes, each calls assignSpot
Dynamic pricingNew FeeCalculator implementation

None of these require modifying existing classes. That is the Open/Closed Principle at work.

What comes next

The library management system introduces state machines for tracking object lifecycles, something the parking lot’s simple occupied/free toggle does not need. It also brings search functionality and fine calculation into the mix.

Start typing to search across all content
navigate Enter open Esc close