Parking Lot LLD — What I Learned Building It

I recently started practicing Low Level Design problems to strengthen my system design and backend thinking.

The second problem I picked was the classic Parking Lot System.

At first, it looked simple.

But while building it, I realized these problems are less about writing code and more about:

  • Modeling real-world systems
  • Managing state correctly
  • Designing scalable abstractions
  • Avoiding bad object-oriented practices

Code Repo parking lot

Requirements

The system supports:

  • Multiple parking floors
  • Different parking spot types:
    • Small
    • Medium
    • Large
  • Multiple vehicle types:
    • Bike
    • Car
    • Truck
  • Ticket generation at entry
  • Fee calculation during exit based on parking duration

Initial Flow

After reading the requirements, this was the initial flow I designed:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
Vehicle Entry
Find Parking Spot
Generate Ticket
Park Vehicle
Vehicle Exit
Calculate Fee
Free Parking Spot

Parking Lot Flow Diagram

Then I converted it into a more structured object-oriented design.

Parking Lot Flow Diagram


Important Things I Learned

1. Static Methods Should Not Modify Instance State

One of the biggest mistakes I made initially was using static methods everywhere.

My first implementation looked something like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public class ParkingLot {

    private static List<Ticket> tickets = new ArrayList<>();

    public static void entry(Vehicle vehicle) {

        Optional<ParkingSpot> spot = findSpot(vehicle.getVehicleType());

        Ticket ticket = new Ticket(vehicle, spot.get());
        tickets.add(ticket);
    }
}

This was incorrect because:

  • tickets represents the state of a parking lot object
  • parking operations belong to a parking lot instance
  • static methods should not manage mutable business state

I later refactored the design to use instance methods instead:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ParkingLot {

    private final List<Ticket> tickets =
        new ArrayList<>();

    public Optional<Ticket> entry(Vehicle vehicle) {

        Optional<ParkingSpot> spot =findSpot(vehicle.getVehicleType());

        if (spot.isEmpty()) {
            System.out.println("No spot available for: " + vehicle.getVehicleType());
            return Optional.empty();
        }

        Ticket ticket =new Ticket(vehicle, spot.get());
        tickets.add(ticket);

        spot.get().bookSpot(ticket.getTicketId());

        return Optional.of(ticket);
    }
}

This made the design much cleaner and aligned better with object-oriented principles.


2. Making Singleton Thread Safe

Since a parking lot should ideally have a single system instance, I implemented the Singleton pattern.

To make it thread-safe, I used:

  • volatile
  • synchronized block
  • double-checked locking
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
private static volatile ParkingLot instance;

public static ParkingLot init(List<Floor> floors,FeeStrategy feeStrategy) {

    if (instance == null) {

        synchronized (ParkingLot.class) {
            if (instance == null) {
                instance =new ParkingLot(floors, feeStrategy);
            }
        }
    }

    return instance;
}

One thing I learned here:

volatile prevents instruction reordering and ensures all threads see the fully initialized object correctly.

Before this, I only knew Singleton theoretically.

Implementing it properly helped me understand concurrency concepts much better.


3. Small Real-World Improvements Matter

Initially, I only supported exiting using a ticket ID. But in real parking systems, users often lose tickets. Parking attendants usually search using vehicle registration numbers.

So I added:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public void exitByRegNumber(String regNumber) {

    Ticket ticket = tickets.stream()
        .filter(t ->
            t.getVehicle()
             .getRegistrationNumber()
             .equals(regNumber)
            && t.getExitTime() == null
        )
        .findFirst()
        .orElseThrow(() ->
            new IllegalArgumentException(
                "No active ticket for: "
                + regNumber
            )
        );

    exit(ticket.getTicketId());
}

This also made me think about scalability. Right now this performs a linear search:

1
O(n)

For a production-scale system, maintaining:

1
Map<String, Ticket>

for active vehicles would provide constant-time lookups. That was another good reminder:

  • System design is not only about classes and diagrams
  • Choosing the right data structures matters equally

Final Thoughts

This problem looked easy at first.

But while implementing it, I learned:

  • Better object-oriented design
  • Singleton implementation
  • Concurrency basics
  • Importance of data structures
  • Thinking beyond the “happy path”

Next, I’m planning to work on:

  • BookMyShow LLD
  • Food Delivery System