BackendMedium35 min

Design a Parking Lot (LLD)

Model entities, relationships, and pricing so new vehicle types or rules never force a rewrite.

Say it in 30 seconds
  • 1,000 spotsfive floors of 200 spots each
  • ~500 / hourentries at peak, under one per second
  • O(types)allocation cost with free sets per type
Show what to say

“I'd model ParkingLot, Floor and Spot, and keep a free set per spot type on each floor, so allocating a spot never scans. Spot choice and pricing sit behind small strategies, with a daily cap as a decorator. One Node process allocates atomically; across gate servers the database picks and locks a row with SKIP LOCKED.”

Trade-offs

  • Free sets per type vs a scan: allocation is O(types) and pays off past hundreds of spots, but there is more state to keep consistent.
  • Strategy classes vs if/else: rules are swappable and testable, and rules do change, but you get more files.
  • In-memory vs DB as source of truth: simple and fast for one gate controller, but it only works in one process.

Bring it up before they ask

Two gates must never hand out the same spot: an atomic take() in one process, SKIP LOCKED across servers, with the ticket inserted in the same transaction so a crash cannot leak a spot.

0 of 0 sections ticked
30-second answer

Model ParkingLot → Floor → Spot, put spot choice and pricing behind small swappable rules, and keep a free list per spot type so nothing scans. Across gate servers, the database locks the spot with FOR UPDATE SKIP LOCKED.

Functional

  • Bike, compact and large spots on several floors
  • A ticket at entry, the fee at exit
  • Free spots per floor and type on display boards
  • Several payment methods and gates

Non-functional

  • Never give a spot out twice, even with concurrent gates
  • No full scan to allocate
  • New vehicle types or pricing rules: no edits to core classes
  • Deterministic tests (injected clock)

Out of scope

  • Reservations (a follow-up)
  • Plate-recognition hardware
  • A chain of lots (an HLD follow-up)

A low-level design (LLD) question: pick the classes first, then find where two gates collide.

New to this? Read From requirements to classes, SOLID and Swap behaviour first.

Scope, scale and interface

Ask: how many floors and spots?

Read more · 148 words

Which vehicles fit which spots? How is the fee worked out? Where do drivers pay? One lot, or a chain?

Five floors of 200 spots is 1,000 spots and about 500 cars an hour. One small process can run it. The hard part is two gates never giving out the same spot.

A scan of 1,000 spots takes microseconds, so speed alone does not justify free sets. They keep the smallest-first rule simple and the cost flat as lots grow past hundreds of spots.

The interface:

CallReturnsErrors
park(vehicle)Ticket with id, spot and entry timeLot full for car
quote(ticketId)Fee owed right nowUnknown ticket
exit(ticketId, payment){ fee, change }, and the spot is freedPayment of 40 required, Unknown ticket
floor.available(type)Free spots of that type on the floor
event changed(floor)Fired after every park and exit

Build the design in stages

Build it like a whiteboard sketch: nouns first, then move each rule that will change behind an interface.

Read more · 111 words

Green marks what the latest stage added.

Not every rule needs a class. Do the sum on pricing, at 20 an hour for a car:

Rules todayCode
Hourly onlyOne function
Plus a 150 daily capBorderline, one if reads fine
Plus weekend rates, EV charging, monthly changesStrategies, cap as a decorator

A car parked 8 hours owes 160 hourly, so the cap bites. The second rule is the tipping point. Until it exists, a function is enough.

What it remembers:

  • ParkingLot.floors: floors by level.
  • Floor.free: spot type → set of free spots.
  • ParkingLot.tickets: open tickets by id. Exit deletes the entry, so a ticket works once.
The decorator pattern is nested gift wrap: each layer adds something around the same gift without changing it.

Stage 1 of 5

The nouns

Start with the nouns in the requirements. A lot owns floors, floors own spots, and a ticket links a vehicle to the spot it took.

Parking lot class diagramParkingLot owns its floors and open tickets, and floors own their spots. Spot choice, pricing and payment sit behind small interfaces, so new rules are new classes, not edits. Card, Cash and Wallet implement PaymentMethod.Vehicletype: VehicleTypeplate: stringneedsCharger?: booleanTicketid: uuidvehicle: Vehiclespot: Spotentry: timestampParkingLotfloors: Floor[]tickets: Map<id, Ticket>selector, pricing, clockpark(vehicle): Ticketquote(ticketId): numberexit(ticketId, payment)Spotid: stringtype: SpotTypefloor: numberfeatures: SetFloorlevel: numberfree: Map<type, Set>take(types, accept)release(spot)available(type)1..*1..*open

ParkingLot owns its floors and open tickets, and floors own their spots. Spot choice, pricing and payment sit behind small interfaces, so new rules are new classes, not edits. Card, Cash and Wallet implement PaymentMethod.

  • ◆ composition (owns)
  • ◇ aggregation
  • △ inheritance / implements (dashed)
  • → association · ⇢ dependency

Watch it run

Park a few cars, then click one to pay.

Read more · 35 words

Try Two cars, one spot on each gate setting: an atomic take() turns the second car away, but check-then-take sends both to 0-C3.

Then play the sequence, including the lot full and payment failed branches.

Interactive

Parking lot simulator

Three floors, two gates, one pay booth. Click a parked vehicle to send it out.

Add
Floor 0B2C4L2Floor 1B3C5L2Floor 2B3C6L3✓ SPACES
Clock 08:00 (1 s ≈ 10 min)Free 30 of 36Revenue 0Turned away 0

6 vehicles are parked. Add one, click a parked one to send it to the exit, or press Run for rush hour.

More controls and event log
Spot selector
Event log
  1. Nothing yet: add a vehicle.

Pricing: first 10 min free, then every started hour (bike 10, car 20, truck 40), at most 150 a day. Bays: B bike, C compact, L large; a dashed bay is held by a ticket with no vehicle in it yet. Each vehicle stays about the typical stay; click or press Enter on a parked one to send it out early.

Press Play to watch it run · 17 steps

A car enters, the selector takes a free spot, and the gate prints a ticket. Later the gate quotes the fee, the driver pays, and exit frees the spot. Pick a branch to see a full lot or a failed payment.

Park, then exitA car enters, the selector takes a free spot, and the gate prints a ticket. Later the gate quotes the fee, the driver pays, and exit frees the spot. Pick a branch to see a full lot or a failed payment.Entrypark({ type: 'car' })choose(floors, car)take([compact, large])alt [spot found]spot 0-C1spot 0-C1tickets.set(id, ticket)emit('changed', 0): boards show one fewer compact spotticket (id, spot, entry)Exit, 61 minutes laterquote(ticketId)fee(61 min, car)40 (2 started hours)fee: 40pay(40)alt [payment ok]paid: amount 40exit(ticketId, payment)tickets.delete(id)release(spot 0-C1)emit('changed', 0): boards show the spot free againfee 40, change 0
  • Client
  • Service
  • Cache
  • External API
Text description
  1. Gate → ParkingLot: park({ type: 'car' })
  2. ParkingLot → Selector: choose(floors, car)
  3. Selector → Floor: take([compact, large])
  4. Floor replies to Selector: spot 0-C1
  5. Selector replies to ParkingLot: spot 0-C1
  6. ParkingLot: tickets.set(id, ticket)
  7. ParkingLot replies to Gate: ticket (id, spot, entry)
  8. Gate → ParkingLot: quote(ticketId)
  9. ParkingLot → Pricing: fee(61 min, car)
  10. Pricing replies to ParkingLot: 40 (2 started hours)
  11. ParkingLot replies to Gate: fee: 40
  12. Gate → Payment: pay(40)
  13. Payment replies to Gate: paid: amount 40
  14. Gate → ParkingLot: exit(ticketId, payment)
  15. ParkingLot: tickets.delete(id)
  16. ParkingLot → Floor: release(spot 0-C1)
  17. ParkingLot replies to Gate: fee 40, change 0

Step-by-step answer

  1. Use cases first

    Park, pay, exit, show free spots.

  2. Nouns become classes, verbs become methods

    Spot, Vehicle and Ticket hold data. ParkingLot, Floor and the rules hold behaviour.

  3. Relationships

    The lot owns floors, floors own spots. A ticket only points at a spot.

  4. Allocation without a scan

    FITS lists the spots each vehicle may use, smallest first:

    export const FITS = {
      bike: ['bike', 'compact', 'large'],
      car: ['compact', 'large'],
      truck: ['large'],
    };

    take(types, accept) pops the first free spot that passes accept.

  5. Pricing as a strategy

    fee(durationMs, vehicle) is the whole interface. A daily cap wraps it as a decorator. Pass in the clock.

  6. Payment flow

    Quote, pay, then exit re-checks the amount and frees the spot. A failed payment changes nothing.

  7. Concurrency

    One Node process is atomic for free. Several servers need one statement that picks and locks a row:

    UPDATE spots SET ticket_id = $1, taken_at = now()
    WHERE id = (SELECT id FROM spots WHERE ... AND ticket_id IS NULL
                LIMIT 1 FOR UPDATE SKIP LOCKED)
    RETURNING id, floor;

    Insert the ticket in the same , or a crash loses the spot.

  8. Display boards

    The lot announces changed(floor). Boards listen (an observer).

  9. Extensibility check

    A new vehicle is one FITS row.

  10. Tests

    Lot full, bad ticket, grace period, daily cap, all with a fake clock.

Implementation

The same design in JavaScript, Java or Python. Java and Python add real locks.

Read more · 47 words
  • HourlyPricing charges every started hour: 61 minutes is 2 hours.
  • DailyCap: 26 hours for a car is 150 + 40 = 190.
  • Pick one floor-versus-type policy and use it in both the code and allocate.sql.

Try the Parking Lot exercise: hidden tests, and both rules switch at runtime.

export const VehicleType = Object.freeze({ BIKE: 'bike', CAR: 'car', TRUCK: 'truck' });
export const SpotType = Object.freeze({ BIKE: 'bike', COMPACT: 'compact', LARGE: 'large' });

// Which spot types each vehicle may use, smallest first, so a bike never takes the large spot a truck needs
export const FITS = {
  [VehicleType.BIKE]: [SpotType.BIKE, SpotType.COMPACT, SpotType.LARGE],
  [VehicleType.CAR]: [SpotType.COMPACT, SpotType.LARGE],
  [VehicleType.TRUCK]: [SpotType.LARGE],
};

export class Spot {
  constructor(id, type, floor) { Object.assign(this, { id, type, floor, features: new Set() }); }   // features: 'ev', 'accessible'…
}

Persistence across gate servers

Only if asked. The database is the truth: a spot is free while ticket_id is NULL.

CREATE TABLE spots (
  id        text   PRIMARY KEY,                 -- '2-C14'
  lot_id    bigint NOT NULL,
  floor     int    NOT NULL,
  type      text   NOT NULL CHECK (type IN ('bike', 'compact', 'large')),
  features  text[] NOT NULL DEFAULT '{}',       -- 'ev', 'accessible'
  ticket_id uuid   UNIQUE,                      -- NULL means free
  taken_at  timestamptz
);
CREATE INDEX spots_free ON spots (lot_id, type, floor, id) WHERE ticket_id IS NULL;

CREATE TABLE tickets (
  id       uuid   PRIMARY KEY,
  lot_id   bigint NOT NULL,
  plate    text   NOT NULL,                     -- lets you find a lost ticket
  vehicle  text   NOT NULL,
  spot_id  text   NOT NULL REFERENCES spots (id),
  entry_at timestamptz NOT NULL DEFAULT now(),
  exit_at  timestamptz,
  fee      int
);

Failure modes and trade-offs

Read more · 159 words
What breaksEffectMitigation
Two gates allocate at onceSame spot twiceOne process: atomic take(). Several: SKIP LOCKED
Gate crashes before the ticket printsSpot lostSpot and ticket in one transaction; a cleanup job frees orphans
Lost ticketNo feeFind it by plate, or charge the daily maximum
Gate loses its networkNo central DBA local service works offline and syncs later
Payment declinedDriver stuckNothing changes until payment works; offer another method

Choices to defend:

ChoiceProsCons
Free lists vs a scanFast, even with many spotsMore state to keep right
Strategy classes vs if/elseRules swap and test easilyMore files
Memory vs databaseSimple and fastOne process only
Decorators vs one classRules combine freelyWrapping order matters

Follow-up questions

How would you scale to 500 lots?

Run a local service per lot, offline-safe, syncing to a central one split by lot_id.

How do you handle a lost ticket?

Find the open ticket by plate and charge from the real entry time. No match: charge the daily maximum.

How would you add reservations?

Reserve a spot type for a time window, not one spot. Release no-shows after a grace period.

How would you add EV charging?

Add an ev spot feature, a needsCharger filter and an EnergyFee decorator. No existing class changes.

Is take() really atomic in Node?

In one process, yes. Across processes, allocate in the database (SKIP LOCKED) or (SET NX).

Check yourself

Question 1 of 3

Score 2 out of 3 or better to mark this page complete.

1. What keeps allocation fast as the lot grows?

Patterns behind this

The coding patterns this design is built on. Practise them and the design questions get easier.