Home › Low-Level Design › Design a Parking Lot
D

Design a Parking Lot

This problem looks simple and then quietly tests whether you can model the real world without drowning in classes. It is a classic low-level-design interview, and the pattern is consistent: the candidates who shine ask what kind of parking lot before they draw a single box. The ones who flame out open with an abstract AbstractVehicleEntityManager in the first 90 seconds. The whole game is finding the two things that change (pricing and spot selection), isolating them, and making the spot claim atomic so two gates never double-book.

Low-Level DesignMedium11 min read

If you remember five things

The whole answer in one glance. Everything below is the defense of these five calls.

  1. Thin coordinator, fat objects. ParkingLot owns floors and delegates the two interesting decisions out. A spot owns its own fit rule and its own lock. No god class.
  2. Strategy twice. FeeStrategy and SpotAssignmentStrategy are the two axes of change. Pull them out and the stable core stops getting rewritten when finance reprices or someone adds EV spots.
  3. Check and claim are one atomic step. The single most important sentence in the room. `tryAssign` does both inside one synchronized block, so two gates scanning the same free spot cannot both win.
  4. Size by enum ordinal, not subclass explosion. Spots carry a size enum, vehicles carry a size enum, "fits" is an ordinal compare. No SmallSpot/MediumSpot classes that differ by one field.
  5. Know when NOT to reach for a pattern. No State on the spot, no forced Singleton on the lot. Name the trigger that would change your mind, and you have shown you know where the line is.

1Clarify the requirements#

Stop and scope it before drawing anything. A parking lot can mean a flat lot at a grocery store or a ten-floor airport garage with license-plate cameras.

  • Those are different problems with different data structures and different failure modes.
  • Spend the first 3 to 5 minutes here, not writing code.
  • The answers you get here are load-bearing, so treat them that way.

Here is how I would open:

Candidate: Before I draw anything: is this a flat lot or a multi-floor garage, and do we have different vehicle sizes with spot sizes matched to them?

Interviewer: Multi-floor. Three vehicle sizes, three spot sizes, and a bigger spot can hold a smaller vehicle.

Candidate: Got it. Do we charge money, and if so, flat, hourly, or tiered? And could that pricing change later?

Interviewer: Hourly, and yes, assume finance will reprice it. Design so that does not force a rewrite.

Candidate: Last one, and it is the one that shapes everything: is this a single process, or do many entry gates hit the system at the same time?

Interviewer: Many gates, concurrent. That is where I want you to spend time.

What those answers just told me:

  • The interview is about concurrency on spot allocation and pluggable policy, not CRUD.
  • "Pricing will change" means pull fee calculation behind an interface so the lot never sees a rate table.
  • "Many gates, concurrent" is the interviewer handing me the thesis: the spot claim has to be atomic or two drivers fight over 4B.

Functional requirements

  • Three vehicle sizes: motorcycle, car, van.
  • Three spot sizes: small, medium, large. A vehicle fits a spot of its own size or bigger.
  • Vehicle enters, gets a ticket, parks in a fitting free spot. Vehicle leaves, we compute the fee, free the spot.
  • Multiple floors, each with its own set of spots.
  • Pricing is hourly and can change later without a rewrite.

Non-functional requirements

  • Thread safe. Two gates must never hand out the same spot. This is the single most common correctness bug in this problem.
  • Spot lookup should be fast, not a linear scan of every spot in a 5000-spot building.
  • Pluggable pricing and spot selection, because interviewers always push on this and I want the seams already cut.
From the interviewer's chair

I am scoring this opening before you write a class. I want to hear "is this concurrent?" come from you, not from me. Jump straight into an inheritance tree and I have quietly downgraded you. One opinion worth stating up front: do not model payments as a full gateway here. It eats 10 minutes and earns zero points. Keep a thin Payment abstraction and move on.

2Identify the core entities#

Name the nouns in the requirements and keep only the ones that earn their place. Every class you add is a class the interviewer can ask you to justify, so be stingy.

  • Vehicle (abstract) with subtypes Motorcycle, Car, Van. Each carries a size.
  • ParkingSpot with a size, a flag for occupied, and the vehicle currently in it.
  • ParkingFloor owns a collection of spots and knows how to find a free one.
  • ParkingLot is the top level. It owns floors and is the entry point for park and unpark.
  • Ticket records the vehicle, the spot, and the entry time. This is the object the gate prints.
  • FeeStrategy computes cost from a ticket and exit time. Pluggable.
  • SpotAssignmentStrategy picks which free spot to give out. Pluggable.
  • EntrancePanel / ExitPanel are the operations surface. I fold these into methods on ParkingLot unless the interviewer wants hardware modeled.

Why split the two strategies out instead of baking them into ParkingLot:

  • Those are the two things that change. Everything else is stable.
  • Pricing changes every quarter when finance reprices vans.
  • Spot selection changes the day someone adds "reserve EV spots near the door."
  • Isolate the volatile parts and the stable core stops getting rewritten.
The point of this section

The entity list is table stakes. What earns points is naming the two axes of change (pricing, spot selection) and putting a seam there before the interviewer asks. That one move is the difference between a design that absorbs new requirements and one you rewrite every time the spec shifts.

3Class design#

The shape I aim for: ParkingLot is a thin coordinator.

  • It holds floors by composition: floors do not exist without the lot.
  • It delegates the two interesting decisions to strategy objects.
  • A floor holds spots. A spot holds at most one vehicle. A ticket is a plain record glued to a spot and a vehicle.

Vehicles use inheritance; spots do not:

  • Vehicles get subclasses because polymorphism on type is clean and the only real variation is size.
  • Spots get a size enum, not subclasses. That saves a class explosion of SmallSpot, MediumSpot, LargeSpot that would be identical except for one field.
classDiagram class Vehicle { -String licensePlate -VehicleSize size +getSize() VehicleSize } class Motorcycle class Car class Van class ParkingSpot { -String id -SpotSize size -boolean occupied -Vehicle vehicle +canFit(vehicle) boolean +assign(vehicle) void +release() void } class ParkingFloor { -int floorNumber -List spots +findSpot(vehicle) ParkingSpot } class ParkingLot { -List floors +parkVehicle(vehicle) Ticket +unparkVehicle(ticket) double } class Ticket { -String id -Instant entryTime -ParkingSpot spot -Vehicle vehicle } class FeeStrategy { +calculate(ticket exitTime) double } class HourlyFeeStrategy class SpotAssignmentStrategy { +assign(floors vehicle) ParkingSpot } class NearestFirstStrategy Vehicle <|-- Motorcycle Vehicle <|-- Car Vehicle <|-- Van FeeStrategy <|-- HourlyFeeStrategy SpotAssignmentStrategy <|-- NearestFirstStrategy ParkingLot *-- ParkingFloor ParkingFloor *-- ParkingSpot ParkingSpot o-- Vehicle Ticket --> ParkingSpot Ticket --> Vehicle ParkingLot --> FeeStrategy ParkingLot --> SpotAssignmentStrategy
The static structure. ParkingLot is a thin coordinator wired to two swappable strategies. The arrows carry meaning: *-- is composition (the part dies with the whole), o-- is aggregation (the part outlives the whole).

Read the relationship arrows out loud, because interviewers read them:

  • ParkingLot *-- ParkingFloor is composition: kill the lot and the floors go with it.
  • ParkingSpot o-- Vehicle is aggregation: the vehicle outlives the spot it sat in.
  • Getting that distinction right out loud is a cheap signal that you mean what the diagram says.

Here is the park flow at runtime, which is where the concurrency story lives.

sequenceDiagram participant Driver participant ParkingLot participant Strategy as SpotAssignmentStrategy participant Spot as ParkingSpot Driver->>ParkingLot: parkVehicle(car) ParkingLot->>Strategy: assign(floors car) Strategy->>Spot: canFit(car) Spot-->>Strategy: true Strategy-->>ParkingLot: spot ParkingLot->>Spot: assign(car) ParkingLot-->>Driver: Ticket
The park flow. The strategy finds a candidate and the spot gets claimed. The gap between "canFit returns true" and "assign runs" is exactly where a second gate can sneak in, which is why the real code folds both into one atomic call.
Pause before you read on: in the sequence above, canFit returns true and then assign runs as a separate step. A second gate runs the same two steps at the same instant on the same spot. What breaks?
Reveal the answer
Both gates see canFit true, both call assign, and both drivers get spot 4B. One pulls in to find a car already there. This is a time-of-check-to-time-of-use race. The fix is to collapse "is it free and fits" and "claim it" into one synchronized step (`tryAssign`), so exactly one thread wins and the loser keeps scanning. Reaching for this unprompted is what clears the concurrency bar.

4Key implementation#

Start with the size model and the vehicle hierarchy. I use enums for sizes so fit logic is an ordinal compare, no lookup table.

  • MOTORCYCLE is ordinal 0, VAN is ordinal 2.
  • "Fits" means the spot's rank is at least the vehicle's rank.
public enum VehicleSize { MOTORCYCLE, CAR, VAN }
public enum SpotSize   { SMALL, MEDIUM, LARGE }

public abstract class Vehicle {
    private final String licensePlate;
    private final VehicleSize size;

    protected Vehicle(String licensePlate, VehicleSize size) {
        this.licensePlate = licensePlate;
        this.size = size;
    }
    public VehicleSize getSize() { return size; }
    public String getLicensePlate() { return licensePlate; }
}

public class Motorcycle extends Vehicle {
    public Motorcycle(String plate) { super(plate, VehicleSize.MOTORCYCLE); }
}
public class Car extends Vehicle {
    public Car(String plate) { super(plate, VehicleSize.CAR); }
}
public class Van extends Vehicle {
    public Van(String plate) { super(plate, VehicleSize.VAN); }
}

The spot owns the fit rule and its own locking. I keep the lock on the spot, not the lot.

  • Two threads fighting over one spot resolve locally.
  • A building-wide lock would serialize all 5000 gates behind one mutex. That kills throughput.
public class ParkingSpot {
    private final String id;
    private final SpotSize size;
    private boolean occupied;
    private Vehicle vehicle;

    public ParkingSpot(String id, SpotSize size) {
        this.id = id;
        this.size = size;
    }

    public boolean canFit(Vehicle v) {
        return spotRank(size) >= vehicleRank(v.getSize());
    }

    // atomic claim: returns false if someone beat us to it
    public synchronized boolean tryAssign(Vehicle v) {
        if (occupied || !canFit(v)) return false;
        this.vehicle = v;
        this.occupied = true;
        return true;
    }

    public synchronized void release() {
        this.vehicle = null;
        this.occupied = false;
    }

    public synchronized boolean isFree() { return !occupied; }
    public String getId() { return id; }

    private static int spotRank(SpotSize s) { return s.ordinal(); }
    private static int vehicleRank(VehicleSize s) { return s.ordinal(); }
}

Notice tryAssign. The check and the claim happen inside one synchronized block. That is the whole trick for correctness.

  • Check "is it free" in the strategy and assign later in a separate call, and you have opened a time-of-check-to-time-of-use race.
  • Under two gates it will double-book.
  • Say the words "check and claim are one atomic step" at the whiteboard. That single sentence is worth real points.

The assignment strategy picks a candidate and tries to claim it, retrying if it lost the race. The loop keeps scanning instead of bailing, so a lost race just moves on to the next free spot.

public interface SpotAssignmentStrategy {
    ParkingSpot assign(List<ParkingFloor> floors, Vehicle vehicle);
}

public class NearestFirstStrategy implements SpotAssignmentStrategy {
    @Override
    public ParkingSpot assign(List<ParkingFloor> floors, Vehicle vehicle) {
        for (ParkingFloor floor : floors) {
            for (ParkingSpot spot : floor.getSpots()) {
                if (spot.canFit(vehicle) && spot.tryAssign(vehicle)) {
                    return spot; // claimed atomically
                }
            }
        }
        return null; // lot full for this size
    }
}

Fee calculation lives behind an interface so the interviewer can swap flat, hourly, or weekday/weekend pricing without touching the lot. The hourly rates are concrete on purpose: 10 per hour for a motorcycle, 20 for a car, 40 for a van.

public interface FeeStrategy {
    double calculate(Ticket ticket, Instant exitTime);
}

public class HourlyFeeStrategy implements FeeStrategy {
    private final Map<VehicleSize, Double> ratePerHour = Map.of(
        VehicleSize.MOTORCYCLE, 10.0,
        VehicleSize.CAR, 20.0,
        VehicleSize.VAN, 40.0
    );

    @Override
    public double calculate(Ticket ticket, Instant exitTime) {
        long minutes = Duration.between(ticket.getEntryTime(), exitTime).toMinutes();
        long hours = (long) Math.ceil(minutes / 60.0); // round up, charge a full hour
        double rate = ratePerHour.get(ticket.getVehicle().getSize());
        return Math.max(1, hours) * rate; // minimum one hour
    }
}

Finally the lot ties it together. It holds the strategies and keeps a map from ticket id to ticket so exit is O(1), not a search through every parked vehicle.

public class ParkingLot {
    private final List<ParkingFloor> floors;
    private final SpotAssignmentStrategy assignmentStrategy;
    private final FeeStrategy feeStrategy;
    private final Map<String, Ticket> activeTickets = new ConcurrentHashMap<>();

    public ParkingLot(List<ParkingFloor> floors,
                      SpotAssignmentStrategy assignmentStrategy,
                      FeeStrategy feeStrategy) {
        this.floors = floors;
        this.assignmentStrategy = assignmentStrategy;
        this.feeStrategy = feeStrategy;
    }

    public Ticket parkVehicle(Vehicle vehicle) {
        ParkingSpot spot = assignmentStrategy.assign(floors, vehicle);
        if (spot == null) {
            throw new IllegalStateException("No spot available for " + vehicle.getSize());
        }
        Ticket ticket = new Ticket(UUID.randomUUID().toString(), Instant.now(), spot, vehicle);
        activeTickets.put(ticket.getId(), ticket);
        return ticket;
    }

    public double unparkVehicle(String ticketId) {
        Ticket ticket = activeTickets.remove(ticketId);
        if (ticket == null) {
            throw new IllegalArgumentException("Invalid or already used ticket");
        }
        double fee = feeStrategy.calculate(ticket, Instant.now());
        ticket.getSpot().release();
        return fee;
    }
}

The constructor takes both strategies as arguments, which is dependency injection doing real work:

  • The lot never news up a pricing or assignment policy.
  • In a test I hand it a fake strategy and assert the flow in isolation.
  • That is the testability win from keeping the lot thin.

I leave floors and tickets as plain holders. They are glue, not logic. Padding the design with getters would burn whiteboard time I would rather spend on concurrency, and the shape is already readable from the classes above.

5Design decisions and patterns used#

Strategy, used twice, and that is deliberate.

  • FeeStrategy and SpotAssignmentStrategy are the two axes of change.
  • Pull them out and ParkingLot stops caring how we price or where we park.
  • Swap NearestFirstStrategy for a RandomStrategy or KeepFloorsBalancedStrategy without touching the lot.
  • Swap HourlyFeeStrategy for a weekend rate the same way.

Factory to build vehicles from a gate reading.

  • A VehicleFactory.create(type, plate) keeps the "which subclass" decision in one place.
  • The rest of the code deals in Vehicle and never writes a switch on type scattered across files.

Singleton is tempting for ParkingLot, and I resist the hard version.

  • There is usually one building, so I mention it.
  • A hard singleton makes the thing miserable to test: you cannot stand up two isolated lots in two test cases.
  • I prefer one instance wired at startup via dependency injection. That gives you "one in production" without poisoning the tests.

Observer is my follow-up answer, not my first draft.

  • When the interviewer asks "how does the entrance sign show FULL," an observer on spot count that notifies a display board is the clean move.
  • I do not build it unprompted, because speculative patterns are a red flag.

State, intentionally not used on the spot.

  • A boolean occupied flag plus a vehicle reference covers every case here.
  • State pattern would be over-engineering.
  • I reach for it only if spots gain a real lifecycle (reserved, out of service, charging) with transition rules, and I say that trigger out loud so the interviewer sees I know where the line is.
Why this earns points

Bolting on three patterns you do not need reads as pattern-matching, not judgment. Saying "State is overkill here, I would add it only when spots get a real lifecycle" shows you know what each pattern costs and when it pays. Knowing when NOT to use a pattern is a clearer senior signal than knowing the pattern.

6Handling edge cases and concurrency#

This is the part that separates a passing answer from a strong one, and it is where I spend the back half of the interview.

The double-booking race.

  • Two cars arrive at two gates at the same instant. Both strategies scan and both see spot 4B free.
  • Without protection, both get assigned and one driver finds a car already in "their" spot.
  • tryAssign closes this: the check and the claim are one synchronized unit, so exactly one thread wins and the loser keeps scanning.
  • I lock per spot, not the whole lot, so throughput stays high even with many gates hammering the system.

Full lot.

  • The strategy returns null and parkVehicle throws a clear exception the gate turns into "no space for vans."
  • It can be full for vans but open for motorcycles at the same moment, so the message is size-specific, not a blanket "lot full."

Double exit or reused ticket.

  • activeTickets.remove is atomic in a ConcurrentHashMap.
  • The first exit gets the ticket, the second gets null and we reject it with "Invalid or already used ticket."
  • That same remove-returns-null guard stops a released spot from being released twice, which would otherwise let a ghost vehicle linger.

Clock and rounding.

  • I round the fee up to the full hour and enforce a one-hour minimum, so 1 minute and 59 minutes both bill as one hour.
  • Decide this out loud, because a pedantic interviewer will ask what happens at 61 minutes.
  • Answer: it rounds up to 2 hours, so a car pays 40.

A vehicle in a bigger spot.

  • A motorcycle can take a large spot when nothing else is free. canFit allows it, and it is correct behavior.
  • Note that it wastes a large spot a van might have wanted.
  • If they care, add a strategy that prefers the tightest fit by scanning the smallest eligible size first.

Scaling the lookup.

  • Scanning every spot is fine for a demo but ugly for a 5000-spot garage, where every park is O(n).
  • In the room I would say: keep a per-floor queue of free spots per size, so assignment is a poll off a queue instead of a full scan.
  • I keep the scan in code for clarity and call out the upgrade, because showing I know the honest version is worth more than silently shipping the slow one.
Red flag that fails you here

Hand-waving "we would add a lock somewhere" without pointing at the exact two lines that race. Pointing at tryAssign and naming the time-of-check-to-time-of-use window is the moment I decide you have cleared the concurrency bar. Candidates who stay vague about where the lock goes are the ones who have not actually written concurrent code.

7Extensions and follow-ups#

Where interviewers usually push, and how I answer:

  • Payments. Add a Payment interface with Cash and Card implementations. Keep it behind unparkVehicle so the lot only knows "collect this amount" and never learns about card networks.
  • Reserved and EV spots. A new spot attribute plus a strategy that honors reservations. The strategy seam already supports this, so it is a new class, not a rewrite.
  • License plate entry and lost ticket. Index activeTickets by plate as a secondary map, so a driver who lost the ticket can still exit (for a penalty fee) by scanning the plate.
  • Real-time availability display. Observer pattern: a DisplayBoard subscribes to spot-count changes per floor and per size.
  • Distributed, many buildings. Now the per-spot lock is not enough, because synchronized only guards one JVM. Move spot state to a store with atomic compare-and-set, and tryAssign becomes a conditional DB write (claim only if still free). The class design barely changes, which is the whole point of isolating the claim in one method.
Follow-ups the interviewer will ask (and your answers)
"Two gates hit the same free spot at the same instant. How do you stop a double-book?"
The claim is atomic. `tryAssign` checks occupied-and-fits and sets occupied inside one synchronized block on the spot, so exactly one thread wins and the loser keeps scanning. I lock per spot, not the whole lot, so the other 4999 spots stay fully parallel. Checking "is it free" and then assigning in two separate calls is the bug: that is a time-of-check-to-time-of-use race.
"There are multiple entrances and the lot is huge. How does a gate find a spot without scanning 5000 spots every time?"
Keep a per-floor queue of free spots per size. Assignment becomes a poll off the right-size queue instead of an O(n) scan, and release pushes the spot back on. Each entrance can prefer its nearest floor's queue first, so gates do not all collide on floor 1. The queue poll stays atomic, so the concurrency story is unchanged.
"Finance wants weekend pricing and a flat daily cap. How much of the code changes?"
One new class. Pricing lives behind `FeeStrategy`, so I add a `WeekendCappedFeeStrategy` and inject it at startup. ParkingLot never sees a rate table and nothing else changes. That is the payoff of pulling pricing out as its own axis of change.
"A motorcycle just took your last large spot and now a van cannot park. Is that a bug?"
No, it is correct under `canFit`: a vehicle fits any spot at least its own size. It does waste a large spot, though. If the business cares, I swap in a tightest-fit assignment strategy that scans the smallest eligible size first. Same seam, new strategy, no change to the lot.
"Now make it a chain: many buildings, shared availability. What breaks?"
The per-spot `synchronized` only guards one JVM, so it stops protecting anything across nodes. I move spot state to a store with atomic compare-and-set and turn `tryAssign` into a conditional write: claim only if still free. Because the claim was already isolated in one method, the class design barely moves. That isolation is exactly why the distributed version is a swap, not a rewrite.

What you can now explain in the room

  • Why you scope the lot (floors, sizes, pricing, concurrency) before writing a single class.
  • Why Strategy appears twice, and which two axes of change it isolates.
  • Why check-and-claim must be one atomic step, and exactly which two lines race without it.
  • Why you lock per spot instead of per lot, and what that buys for throughput.
  • Why spots use a size enum while vehicles use subclasses.
  • When to skip Singleton and State, and the exact trigger that would change your mind.

Next, try these LLD problems that reuse the same muscles: Design an Elevator System (state and scheduling strategy), Design a Vending Machine (State pattern done right), and Design a Movie Ticket Booking System (concurrency on seat selection).

Get a prep plan built around your Design a Parking Lot interview

Tell us the role and your interview date. We will send back a dated, day by day plan. Free.

Thanks. Check your inbox, your Design a Parking Lot prep plan is on the way.