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

Rough parking lot flow diagram showing vehicle entry, spot assignment, ticket generation, and exit steps

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

Final object-oriented parking lot system design showing class structure and component relationships


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