LLD · Machine Coding Classic

Low Level Design: Parking Lot System (Complete Java Solution)

The most-asked LLD question, solved the way a strong candidate does it in 45 minutes: crisp requirements, a clean class model, two Strategy seams, thread-safe spot allocation and code that actually runs.

✍️ Pranjal Jain, Ex-Microsoft · IIT Kanpur 📅 10 October 2026 ⏱ 19 min read
Isometric multi-level parking garage with vehicles in color-coded spots and a connected grid of boxes representing a class design
TL;DR
  • Spend the first 5 minutes pinning down scope: vehicle types, spot sizes, floors, gates, pricing rules and payment. Write the agreed list down.
  • The model is small: ParkingLot (facade) → ParkingFloor → ParkingSpot, plus Vehicle, Ticket, Receipt.
  • Two Strategy seams carry the design: SpotAllocationStrategy (which spot) and PricingStrategy (how much). A decorator adds a daily cap; an observer drives display boards.
  • The one real concurrency bug is two gates handing out the same spot. Fix it by making "claim this spot" atomic, not by locking the whole lot.
  • Full runnable Java 17 code below, with a dry run, extensions (EV, reservations, passes, ANPR) and the mistakes that cost candidates the round.

The parking lot question is the "Two Sum" of low level design. Almost every Indian product company and many startups have used it in an LLD or machine coding round, which means interviewers have seen hundreds of answers and know exactly where weak ones break. They are not checking whether you know that a parking lot has floors. They are checking whether you can turn a vague prompt into a small, extensible, correct object model, and whether your code survives the follow-up questions.

This guide is written the way you should answer: requirements first, then entities, then the class diagram, then the patterns that earn their place, then complete code. If you want the broader context on how LLD rounds are run, read the LLD interview preparation guide first; if your round is a timed coding exercise, pair this with the machine coding round guide.

Step 1: Clarify Requirements for the Parking Lot

The prompt is usually one line: "Design a parking lot." The interviewer is waiting to see whether you start coding or start asking. Ask, but ask efficiently: propose sensible defaults and let the interviewer correct you, rather than firing twenty open questions.

Questions worth asking (with the defaults I would propose)

QuestionProposed defaultWhy it matters for the design
Which vehicle types?Bike, car, truckDrives the VehicleType enum and compatibility rules
Which spot sizes, and can a small vehicle use a bigger spot?Small, medium, large; a bike may use any, a car medium or large, a truck only largeAllocation must try preferred size first, then fall back
Multiple floors? Multiple entry/exit gates?Yes and yesMultiple gates means concurrent allocation, which is the main correctness risk
How is the fee computed?Flat first hour + per started hour after, by vehicle type, daily capPricing becomes a strategy, the cap a decorator
Payment modes?Cash, UPI, card at exitA PaymentProcessor interface; no gateway internals
Display boards showing free spots?Yes, per floor and per sizeObserver of park/unpark events
Reservations, EV charging, monthly passes?Out of scope now, design should allow themTells you which seams to leave open

Agreed functional requirements

  1. A vehicle arrives at any entry gate; the system assigns a compatible free spot and issues a ticket. If none is free, it rejects the vehicle.
  2. At exit, the system takes the ticket, computes the fee, collects payment and frees the spot.
  3. The same vehicle cannot be parked twice at the same time, and a ticket cannot be used to exit twice.
  4. Display boards show free spots per size, updated on every park and exit.
  5. Pricing and allocation rules can change without rewriting the core.

Non-functional requirements

This is an in-memory, single-process design (that is what LLD means here). The non-functional points that matter are thread safety for concurrent gates, extensibility for new vehicle types and pricing, and testability, which is why times are passed in rather than read from the system clock inside business logic. If the interviewer pushes toward "a parking app for the whole city with millions of users", that is a high level design question; the HLD vs LLD guide explains how to steer between the two.

Step 2: Identify the Core Entities

A reliable trick is to underline the nouns in your requirements and then delete the ones that carry no behaviour or state of their own. "Gate" survives only as a caller; "display board" survives as a listener; "parking attendant" disappears entirely.

EntityResponsibilityKey state
ParkingLotFacade: park, unpark, availability; owns ticketsfloors, active tickets, parked vehicle numbers, strategies
ParkingFloorGroups spots; answers free countsfloor number, spots by size
ParkingSpotAtomically claimed and releasedid, floor, size, current vehicle
VehicleImmutable value: number + typeregistration number, type
TicketProof of entryid, vehicle, spot, entry time
ReceiptProof of exit and paymentticket id, hours billed, amount
SpotAllocationStrategyFind and claim a spotstateless
PricingStrategyCompute feerate tables
PaymentProcessorCollect moneynone in this design
ParkingEventListenerReact to park/exit (display boards, analytics)none
💡
Don't build a Vehicle class hierarchyA common reflex is abstract class Vehicle with Car, Bike, Truck subclasses. They have no different behaviour, only a different type, so an enum is cleaner. Subclasses are justified only when behaviour differs, for example an ElectricCar that needs a charger. Saying this out loud shows judgment.

Step 3: Parking Lot Class Diagram (UML in Text)

Draw this on the whiteboard or in a comment block before writing code. Arrows show "has" (◆) and "implements" (△) relationships.

┌──────────────────────────────┐ EntryGate ───────►│ ParkingLot │◄─────── ExitGate │ + park(Vehicle, time): Ticket│ │ + unpark(id, time, pay) │ │ + availability() │ └──┬────────┬─────────┬────────┘ ◆ 1..* │ │ │ uses ┌────────────┘ │ └──────────────────┐ ▼ ▼ ▼ ┌──────────────┐ ┌───────────────────────┐ ┌──────────────────┐ │ ParkingFloor │ │SpotAllocationStrategy │ │ PricingStrategy │ │ spotsBySize │ │ allocate(floors, v) │ │ fee(ticket,exit) │ └──────┬───────┘ └──────────△────────────┘ └───────△──────────┘ ◆ 1..* ┌───────┴────────┐ ┌───────┴─────────┐ ▼ LowestFloorFirst MostFreeFloor HourlyPricing DailyCap ┌──────────────┐ (decorator) │ ParkingSpot │ id, floor, size, vehicle │ tryPark(v) │◄──────── Ticket(id, vehicle, spot, entryTime) │ release() │ └──────────────┘ ParkingEventListener △ DisplayBoard

Notice what is not there: no Admin, no Database, no Attendant. Interviewers reward a model that is as small as the requirements allow and no smaller.

Step 4: Design Patterns That Earn Their Place

Strategy pattern for spot allocation

"Which spot should this car get?" has several reasonable answers: the lowest floor (shortest walk), the floor with the most free spots (spreads traffic), the spot nearest the exit, or a reserved block for VIP passes. These are interchangeable algorithms with the same signature, which is the textbook case for Strategy. ParkingLot depends on the interface; the concrete strategy is injected at construction.

Strategy pattern for pricing

Pricing changes more often than any other rule in a real lot: weekend rates, event-day surge at a stadium, mall validation discounts, monthly passes. Putting it behind PricingStrategy means every one of those is a new class, which is the Open/Closed principle in practice. The SOLID principles guide uses exactly this kind of example.

Decorator for the daily cap

A daily cap ("never more than ₹300 per 24 hours for a car") applies on top of whatever base pricing you use. Instead of copying the cap logic into every strategy, wrap: new DailyCapPricing(new HourlyPricing(), caps). The decorator implements the same interface and delegates, then clamps.

Observer for display boards

Display boards, an analytics counter and an "almost full" SMS alert to the operator all want to know when a spot changes. ParkingLot publishes events to a list of ParkingEventListeners and knows nothing about what they do.

Why not Singleton?

Many tutorials make ParkingLot a Singleton. It is not wrong to mention, but a hard-coded getInstance() makes unit tests share state and makes the inevitable "now support 5 lots in Bengaluru" follow-up awkward. Construct one instance at startup and inject it. If the interviewer specifically asks for Singleton, show it, and mention this trade-off. For the full catalogue of patterns and when each is overkill, see the top 10 design patterns for interviews.

Step 5: Complete Java Code for the Parking Lot

This is a single file you can run with java ParkingLotDemo.java on Java 17+. In a real machine coding round you would split it into packages (model, strategy, service); a single file keeps the listing readable here.

Enums, value objects and the spot

Java
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicLong;

public class ParkingLotDemo {

    enum VehicleType { BIKE, CAR, TRUCK }

    enum SpotSize { SMALL, MEDIUM, LARGE }

    /** Allowed spot sizes for a vehicle, in order of preference (smallest fit first). */
    static List<SpotSize> allowedSizes(VehicleType type) {
        return switch (type) {
            case BIKE -> List.of(SpotSize.SMALL, SpotSize.MEDIUM, SpotSize.LARGE);
            case CAR -> List.of(SpotSize.MEDIUM, SpotSize.LARGE);
            case TRUCK -> List.of(SpotSize.LARGE);
        };
    }

    record Vehicle(String number, VehicleType type) { }

    record Ticket(String id, Vehicle vehicle, ParkingSpot spot, LocalDateTime entryTime) { }

    record Receipt(String ticketId, String vehicleNumber, long hoursBilled, long amountRupees) { }

    static final class ParkingSpot {
        private final String id;
        private final int floor;
        private final SpotSize size;
        private Vehicle vehicle; // guarded by "this"

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

        /** Atomically claims the spot. Returns false if someone else got it first. */
        synchronized boolean tryPark(Vehicle v) {
            if (vehicle != null) return false;
            vehicle = v;
            return true;
        }

        synchronized void release() { vehicle = null; }

        synchronized boolean isFree() { return vehicle == null; }

        String id() { return id; }
        int floor() { return floor; }
        SpotSize size() { return size; }

        @Override public String toString() { return id; }
    }

    static final class ParkingFloor {
        private final int number;
        private final Map<SpotSize, List<ParkingSpot>> spotsBySize = new EnumMap<>(SpotSize.class);

        ParkingFloor(int number, int small, int medium, int large) {
            this.number = number;
            addSpots(SpotSize.SMALL, small);
            addSpots(SpotSize.MEDIUM, medium);
            addSpots(SpotSize.LARGE, large);
        }

        private void addSpots(SpotSize size, int count) {
            List<ParkingSpot> list = new ArrayList<>();
            for (int i = 1; i <= count; i++) {
                list.add(new ParkingSpot("F" + number + "-" + size.name().charAt(0) + i, number, size));
            }
            spotsBySize.put(size, List.copyOf(list));
        }

        int number() { return number; }

        List<ParkingSpot> spots(SpotSize size) { return spotsBySize.get(size); }

        long freeCount(SpotSize size) {
            return spotsBySize.get(size).stream().filter(ParkingSpot::isFree).count();
        }
    }

The spot list per floor is immutable (List.copyOf): spots are created once and only their occupancy changes. That removes a whole class of concurrency bugs, because nobody can add or remove spots while an allocator iterates.

Allocation strategies

Java
    interface SpotAllocationStrategy {
        /** Finds AND claims a compatible spot, or returns empty if the lot is full for this vehicle. */
        Optional<ParkingSpot> allocate(List<ParkingFloor> floors, Vehicle vehicle);
    }

    /** Shortest walk: lowest floor first, smallest compatible size first. */
    static final class LowestFloorFirst implements SpotAllocationStrategy {
        @Override
        public Optional<ParkingSpot> allocate(List<ParkingFloor> floors, Vehicle vehicle) {
            for (ParkingFloor floor : floors) {
                for (SpotSize size : allowedSizes(vehicle.type())) {
                    for (ParkingSpot spot : floor.spots(size)) {
                        if (spot.tryPark(vehicle)) return Optional.of(spot);
                    }
                }
            }
            return Optional.empty();
        }
    }

    /** Spreads load: try floors in descending order of free spots of the preferred size. */
    static final class MostFreeFloorFirst implements SpotAllocationStrategy {
        @Override
        public Optional<ParkingSpot> allocate(List<ParkingFloor> floors, Vehicle vehicle) {
            for (SpotSize size : allowedSizes(vehicle.type())) {
                List<ParkingFloor> ordered = new ArrayList<>(floors);
                ordered.sort(Comparator.comparingLong((ParkingFloor f) -> f.freeCount(size)).reversed());
                for (ParkingFloor floor : ordered) {
                    for (ParkingSpot spot : floor.spots(size)) {
                        if (spot.tryPark(vehicle)) return Optional.of(spot);
                    }
                }
            }
            return Optional.empty();
        }
    }

The important design decision is that allocate finds and claims in one step through tryPark. If it only "found" a free spot and the caller parked later, two gates could find the same spot in between. Free counts read during sorting may be slightly stale under concurrency, and that is fine: they only affect preference order, never correctness.

Pricing strategy and the daily-cap decorator

Java
    interface PricingStrategy {
        long fee(Ticket ticket, LocalDateTime exitTime);
    }

    /** Billable hours: every started hour counts, minimum one hour. */
    static long billableHours(LocalDateTime entry, LocalDateTime exit) {
        long minutes = Math.max(0, Duration.between(entry, exit).toMinutes());
        return Math.max(1, (minutes + 59) / 60);
    }

    /** Flat first hour + rate for each additional started hour, per vehicle type. */
    static final class HourlyPricing implements PricingStrategy {
        private final Map<VehicleType, long[]> rates = new EnumMap<>(VehicleType.class);

        HourlyPricing() {
            rates.put(VehicleType.BIKE, new long[] {20, 10});   // first hour, each extra hour (Rs)
            rates.put(VehicleType.CAR, new long[] {50, 30});
            rates.put(VehicleType.TRUCK, new long[] {100, 60});
        }

        @Override
        public long fee(Ticket t, LocalDateTime exitTime) {
            long hours = billableHours(t.entryTime(), exitTime);
            long[] r = rates.get(t.vehicle().type());
            return r[0] + (hours - 1) * r[1];
        }
    }

    /** Decorator: never charge more than a cap per started 24-hour block. */
    static final class DailyCapPricing implements PricingStrategy {
        private final PricingStrategy inner;
        private final Map<VehicleType, Long> capPerDay;

        DailyCapPricing(PricingStrategy inner, Map<VehicleType, Long> capPerDay) {
            this.inner = inner;
            this.capPerDay = capPerDay;
        }

        @Override
        public long fee(Ticket t, LocalDateTime exitTime) {
            long days = (billableHours(t.entryTime(), exitTime) + 23) / 24;
            long cap = capPerDay.get(t.vehicle().type()) * days;
            return Math.min(inner.fee(t, exitTime), cap);
        }
    }

Payment, listeners and the ParkingLot facade

Java
    interface PaymentProcessor {
        boolean pay(String ticketId, long amountRupees);
    }

    static final class UpiPayment implements PaymentProcessor {
        @Override public boolean pay(String ticketId, long amount) {
            System.out.println("UPI collect of Rs " + amount + " for " + ticketId + " succeeded");
            return true;
        }
    }

    interface ParkingEventListener {
        void onParked(Ticket ticket);
        void onExited(Ticket ticket, Receipt receipt);
    }

    static final class DisplayBoard implements ParkingEventListener {
        private final ParkingLot lot;
        DisplayBoard(ParkingLot lot) { this.lot = lot; }
        @Override public void onParked(Ticket t) { show(); }
        @Override public void onExited(Ticket t, Receipt r) { show(); }
        private void show() { System.out.println("  [Board] free spots: " + lot.availability()); }
    }

    static final class ParkingFullException extends RuntimeException {
        ParkingFullException(String msg) { super(msg); }
    }

    static final class ParkingLot {
        private final List<ParkingFloor> floors;
        private final SpotAllocationStrategy allocation;
        private final PricingStrategy pricing;
        private final Map<String, Ticket> activeTickets = new ConcurrentHashMap<>();
        private final Set<String> parkedVehicles = ConcurrentHashMap.newKeySet();
        private final List<ParkingEventListener> listeners = new CopyOnWriteArrayList<>();
        private final AtomicLong ticketSeq = new AtomicLong();

        ParkingLot(List<ParkingFloor> floors, SpotAllocationStrategy allocation, PricingStrategy pricing) {
            this.floors = List.copyOf(floors);
            this.allocation = allocation;
            this.pricing = pricing;
        }

        void addListener(ParkingEventListener l) { listeners.add(l); }

        Ticket park(Vehicle vehicle, LocalDateTime at) {
            if (!parkedVehicles.add(vehicle.number())) {
                throw new IllegalStateException(vehicle.number() + " is already parked");
            }
            Optional<ParkingSpot> spot = allocation.allocate(floors, vehicle);
            if (spot.isEmpty()) {
                parkedVehicles.remove(vehicle.number());
                throw new ParkingFullException("No spot for " + vehicle.type());
            }
            Ticket ticket = new Ticket("T" + ticketSeq.incrementAndGet(), vehicle, spot.get(), at);
            activeTickets.put(ticket.id(), ticket);
            listeners.forEach(l -> l.onParked(ticket));
            return ticket;
        }

        Receipt unpark(String ticketId, LocalDateTime at, PaymentProcessor payment) {
            Ticket ticket = activeTickets.remove(ticketId);   // atomic: a ticket can exit only once
            if (ticket == null) throw new IllegalArgumentException("Invalid or used ticket " + ticketId);
            long amount = pricing.fee(ticket, at);
            if (!payment.pay(ticketId, amount)) {
                activeTickets.put(ticketId, ticket);           // roll back so the driver can retry
                throw new IllegalStateException("Payment failed for " + ticketId);
            }
            ticket.spot().release();
            parkedVehicles.remove(ticket.vehicle().number());
            Receipt receipt = new Receipt(ticketId, ticket.vehicle().number(),
                    billableHours(ticket.entryTime(), at), amount);
            listeners.forEach(l -> l.onExited(ticket, receipt));
            return receipt;
        }

        Map<SpotSize, Long> availability() {
            Map<SpotSize, Long> free = new EnumMap<>(SpotSize.class);
            for (SpotSize s : SpotSize.values()) {
                free.put(s, floors.stream().mapToLong(f -> f.freeCount(s)).sum());
            }
            return free;
        }
    }

    public static void main(String[] args) {
        List<ParkingFloor> floors = List.of(
                new ParkingFloor(0, 1, 1, 1),
                new ParkingFloor(1, 2, 2, 1));
        PricingStrategy pricing = new DailyCapPricing(new HourlyPricing(),
                Map.of(VehicleType.BIKE, 100L, VehicleType.CAR, 300L, VehicleType.TRUCK, 600L));
        ParkingLot lot = new ParkingLot(floors, new LowestFloorFirst(), pricing);
        lot.addListener(new DisplayBoard(lot));

        LocalDateTime t0 = LocalDateTime.of(2026, 10, 10, 9, 0);
        Ticket car1 = lot.park(new Vehicle("KA01AB1234", VehicleType.CAR), t0);
        Ticket car2 = lot.park(new Vehicle("KA05MN4321", VehicleType.CAR), t0.plusMinutes(2));
        Ticket bike = lot.park(new Vehicle("MH12XY9876", VehicleType.BIKE), t0.plusMinutes(5));
        System.out.println(car1.spot() + " " + car2.spot() + " " + bike.spot());

        System.out.println(lot.unpark(car1.id(), t0.plusMinutes(150), new UpiPayment()));
        System.out.println(lot.unpark(car2.id(), t0.plusHours(30), new UpiPayment()));
        try {
            lot.unpark(car1.id(), t0.plusMinutes(200), new UpiPayment());
        } catch (IllegalArgumentException e) {
            System.out.println("Rejected: " + e.getMessage());
        }
    }
}

Step 6: Dry Run of the Demo

Walking the interviewer through a dry run is the fastest way to prove your code works without a compiler. Floor 0 has spots F0-S1, F0-M1, F0-L1; floor 1 has F1-S1, F1-S2, F1-M1, F1-M2, F1-L1.

  1. Car 1 at 09:00. LowestFloorFirst starts at floor 0, preferred size MEDIUM, F0-M1.tryPark succeeds. Ticket T1. Board: SMALL 3, MEDIUM 2, LARGE 2.
  2. Car 2 at 09:02. Floor 0 MEDIUM is taken, so it falls back to floor 0 LARGE: F0-L1. Ticket T2. This is a design choice worth flagging: lowest-floor-first will put a car in a truck spot before going upstairs. If trucks are common, swap the loop order (size outer, floor inner) or use MostFreeFloorFirst. Being able to change that by swapping a class is the whole point of the strategy.
  3. Bike at 09:05. F0-S1. Ticket T3.
  4. Car 1 exits at 11:30. 150 minutes → 3 billable hours → ₹50 + 2 × ₹30 = ₹110. Cap for 1 day is ₹300, so ₹110 is charged. Spot F0-M1 is released.
  5. Car 2 exits next day at 15:00. 29 h 58 min → 30 billable hours. Hourly would be ₹50 + 29 × ₹30 = ₹920. It spans 2 started days, so cap = ₹600. Charged ₹600.
  6. Car 1's ticket is presented again. activeTickets.remove returns null, so the second exit is rejected. No double release, no double charge.
O(S)
Worst-case allocation, S = total spots
O(1)
Exit lookup by ticket id
2
Strategy seams (allocation, pricing)

If the interviewer asks to make allocation O(1), keep a free-spot queue per (floor, size): a ConcurrentLinkedQueue<ParkingSpot> you poll() to allocate and offer() to release. Poll is atomic, so it also solves the concurrency problem. The trade-off is that "nearest spot" ordering becomes harder; a PriorityBlockingQueue ordered by distance gives O(log n) with ordering.

Step 7: Concurrency with Multiple Entry Gates

The interviewer's favourite follow-up: "Gate A and Gate B both see spot F1-M1 free at the same instant. What happens?" In a naive design both write a ticket for the same spot and two cars meet at one bay. The fix options, from simplest to most scalable:

ApproachHowTrade-off
Lock the whole lotsynchronized park()Correct but serialises every gate; fine for a small lot, weak answer
Atomic claim per spot (used above)synchronized tryPark returns false if takenGates only contend on the same spot; simple and correct
Compare-and-setAtomicReference<Vehicle>.compareAndSet(null, v)Lock-free; same semantics as above
Free-spot queue per floor/sizeConcurrentLinkedQueue.poll()O(1) and atomic; ordering is FIFO not nearest

Two more races are handled in the code and worth pointing out. The same vehicle entering through two gates is blocked by parkedVehicles.add, which is atomic on a concurrent set. The same ticket exiting twice is blocked because ConcurrentHashMap.remove returns the ticket to exactly one caller. If your interviewer goes deeper into Java memory model questions, the Java multithreading interview questions post covers synchronized, CAS and concurrent collections in detail.

⚠️
Payment failure is a concurrency case tooIn unpark the ticket is removed before payment, so a second exit attempt cannot race in. If payment fails, it is put back. Candidates who free the spot before payment succeeds create a state where the car is still in the bay but the system thinks it is free.

Step 8: Extensions Interviewers Ask (and Where They Plug In)

A good design is measured by how little existing code changes when requirements grow. For each follow-up, say which class you add and which you leave untouched.

EV charging spots

Add SpotSize-independent features: give ParkingSpot a Set<SpotFeature> (CHARGER, COVERED, ACCESSIBLE), and give Vehicle required features. The allocation strategy filters on spot.features().containsAll(vehicle.needs()). Charging cost becomes a second pricing component: a CompositePricing that sums parking fee and kWh fee.

Reservations

A spot gains a third state, RESERVED, with a reservation id and expiry. tryPark accepts a reserved spot only for the vehicle holding that reservation. A scheduled sweeper releases expired reservations. Now spot state has three values and legal transitions, which is a natural place to mention the State pattern, though an enum plus guarded transitions is usually enough.

Monthly passes and FASTag/ANPR entry

Monthly passes are a PricingStrategy that returns 0 when the vehicle number has an active pass and delegates otherwise, which is another decorator. Automatic number plate recognition or FASTag at the gate changes only how the Vehicle is identified and how PaymentProcessor collects; ParkingLot does not change.

Lost ticket

Because parkedVehicles is keyed by registration number, add an index from vehicle number to ticket, look the ticket up by number, and apply a LostTicketPricing decorator that charges the maximum daily rate as a penalty.

Multiple lots in a city

This is why we avoided Singleton. A ParkingLotRegistry maps lot id to ParkingLot. Beyond that, persistence, a booking app and city-wide search are high level design topics; the patterns for reservations with contention are covered in BookMyShow seat booking design.

The 45-Minute Parking Lot Interview Playbook

0–5 min

Requirements

Propose defaults, confirm vehicle types, spot sizes, gates, pricing, payment. Write them down and mark what is out of scope.

5–12 min

Entities + diagram

List entities with one-line responsibilities. Sketch the diagram. Name the two strategies explicitly.

12–35 min

Code the core

Enums, Spot with atomic claim, Floor, one allocation strategy, one pricing strategy, the facade. Skip getters you do not need.

35–45 min

Dry run + extensions

Run one park/exit cycle aloud, then handle concurrency and one or two extensions by naming the class you would add.

What interviewers are actually scoring

  • Scoping: did you ask the questions that change the design (spot compatibility, multiple gates) and skip the ones that don't (colour of the ticket)?
  • Responsibility placement: fee logic in pricing, not in Ticket or Vehicle; claim logic on the spot, not scattered.
  • Extensibility you can demonstrate: "weekend pricing is a new class; nothing else changes" is better than drawing ten interfaces up front.
  • Correctness under concurrency: at mid and senior levels this is often the deciding signal.
  • Code quality: immutability for value objects, clear names, no god class, no stringly-typed vehicle types.

If you can explain your design decisions clearly while coding, you are already ahead of most candidates; the think-out-loud communication guide applies to LLD as much as DSA.

Common Mistakes in Parking Lot LLD

  1. Starting with classes before requirements. You end up modelling an attendant, a database and a CCTV camera that nobody asked for.
  2. Vehicle subclasses with no behaviour. Car extends Vehicle that only overrides getType() is an enum in disguise.
  3. Pricing inside ParkingLot with an if-else ladder. Every new rule edits the core class, which is exactly what the interviewer will ask you to change next.
  4. Find-then-park. Returning a free spot and claiming it later is a race. Claim atomically.
  5. Freeing the spot before payment. Leaves the system inconsistent on payment failure.
  6. Reading LocalDateTime.now() inside pricing. Untestable; pass the time in or inject a Clock.
  7. Over-engineering. Abstract factories for spots, a builder for tickets and five layers of interfaces in a 45-minute round signal that you cannot prioritise. Add patterns when a requirement demands them.
  8. Floating-point money. Use long in rupees or paise, or BigDecimal; never double.

Once parking lot feels comfortable, the same skills transfer directly: the elevator system adds scheduling and state, and the vending machine is the cleanest way to practise the State pattern.

Frequently Asked Questions

What classes are needed for a parking lot low level design?▾
At minimum: ParkingLot (the facade), ParkingFloor, ParkingSpot, Vehicle, Ticket, a SpotAllocationStrategy, a PricingStrategy and a PaymentProcessor. Enums for VehicleType and SpotSize keep the compatibility rules explicit. Gates and display boards are thin callers or observers of the ParkingLot.
Which design pattern is used in parking lot design?▾
Strategy is the core pattern: one strategy decides which spot a vehicle gets and another decides how much it pays. Observer is commonly used for display boards, Factory for creating spots or vehicles, and some candidates use Singleton for the lot, though injecting a single instance is more testable.
Should ParkingLot be a Singleton?▾
It is acceptable to mention, but most interviewers prefer you avoid a hard Singleton because it makes testing and multi-lot extensions painful. Create one instance at startup and pass it to the gates instead; you get the same 'only one lot' behaviour without global state.
How do you handle two cars getting the same spot at the same time?▾
Make the act of claiming a spot atomic. The simplest correct approach is a synchronized tryPark method on the spot that returns false if it is already taken, so the allocator moves on to the next candidate. At larger scale you would use per-floor locks or a compare-and-set on an AtomicReference.
How is parking fee calculated in the LLD?▾
Behind a PricingStrategy interface. A typical implementation charges a flat first hour and a rate for every additional started hour, per vehicle type, with an optional daily cap added via a decorator. Keeping pricing behind an interface lets you add weekend, event or dynamic pricing without touching ParkingLot.
Is parking lot design asked in machine coding rounds in India?▾
Yes. It is one of the most commonly reported LLD and machine coding prompts at Indian product companies and startups. In machine coding the expectation is working, demoable code in 60 to 90 minutes, so keep the model small and make the extension points obvious.
What follow-up questions are asked after parking lot design?▾
Common ones are EV charging spots, reservations, multiple entry gates with concurrency, lost tickets, monthly passes, ANPR or FASTag based entry, and supporting several lots in one city. Each should be absorbable by adding a class or strategy rather than editing existing ones.

Crack LLD and Machine Coding Rounds

Prepflix's interview course walks through parking lot, elevator, Splitwise and a dozen more LLD problems with live code reviews so you learn the judgment, not just the answer.

Explore the Prepflix Course →
Pranjal Jain
Pranjal Jain

Ex-Microsoft SDE · IIT Kanpur · Founder of Prepflix. Helps engineers crack startup and product company interviews across India.