# Design a Parking Lot System | LLD System Design

"Design a parking lot" is the most common low-level design question there is, and it is a trap in two directions. Say too little and it looks like you have a `Spot[]` and a `for` loop. Say too much and you are still drawing classes when the hour ends. This post walks the whole thing the way a real 60-minute machine-coding round goes: pin the scope with questions, turn the requirements into classes, pick the two places a pattern actually earns its keep, and write only the code that carries risk. The full runnable TypeScript is in the repo; here we embed just the pieces that matter.

## Start by asking how big

The first job is not to code. It is to find out which problem you are being asked to solve, because "a parking lot" spans three orders of magnitude. Open the scale question deliberately, and be ready for the interviewer to aim high.

**You:** How big is this lot? A ten-space lot behind a shop and a 20,000-space airport structure are almost different problems.

**Interviewer:** Design for the biggest one. Think a large international airport: around 20,000 spaces spread over several multi-level buildings, with many entry and exit lanes running at the same time.

**You:** Good, that tells me what has to be robust. At that size a few things stop being free. First, spot lookup: scanning 20,000 spots on every car is wasteful, so I want a free-count per spot type per level and a way to hand out a spot without a full scan. Second, and more important, many gates admit cars concurrently, so two lanes can race for the last compact spot at the same instant. Getting that race right is the real core of this design. The 20,000-spot storage itself is a data-partitioning problem I would push behind the object model, not into it.

**Interviewer:** Agreed. What about pricing?

**You:** Is there a fixed rate, or does it vary?

**Interviewer:** You decide.

**You:** Then I will assume it varies by vehicle type and by how long you stayed, rounded up to the hour with a daily cap, because that is what real lots do. I will make pricing a pluggable rule rather than an `if` block, so weekend surge or first-15-minutes-free is a new class, not a rewrite.

**Interviewer:** Cars pay by card at the exit. Handle the payment gateway.

**You:** I will model the fee calculation and the exit flow, but I will treat the actual card-network call as an external dependency behind an interface and stub it. Integrating a real gateway is plumbing, not design, and it would eat the hour.

That exchange does three things an interviewer is grading: it shows a clear answer (vehicle and spot types), it turns a punt ("you decide") into a stated assumption instead of silence, and it takes a curveball (airport scale, real payments) and names the cost before deferring it. If you want the general shape of this scoping move, I wrote it up separately in the [60-minute interview framework](/60-mins-system-design-interview-framework); here we apply it and move on.

### Scope, written down

After the questions, state the scope out loud so you and the interviewer are building the same thing.

In scope:

- One parking lot with one or more levels; each level has spots of a few sizes (small, compact, large).
- Vehicle types: motorcycle, car, truck. A vehicle fits its own spot size or any larger one.
- Multiple entry and exit gates operating at once. Two gates must never hand out the same spot.
- Entry: assign a spot, issue a ticket. Exit: price the stay, free the spot.
- Spot assignment and pricing are both pluggable policies.

Explicitly out of scope (named, then set aside):

- Sharding 20,000 spots across nodes, and the database behind it. I will point at where it plugs in.
- Real payment-gateway integration, receipts, refunds.
- License-plate recognition, reservations, EV charging, handicap-reserved spots. All of these are new `SpotType`/`VehicleType` values or a new strategy later, so the design must not preclude them, but I will not build them now.

Cutting the airport down to "one lot, a few levels, get the concurrency right" is the whole move. It keeps everything that makes the problem interesting and drops everything that only makes it long.

## From requirements to classes

The nouns in the requirements become the classes. Read the scope back and the model almost falls out:

- **Vehicle** (a motorcycle, a car, a truck) - an abstract base with three subclasses.
- **ParkingSpot** - has a size, is free or taken, holds one vehicle.
- **ParkingLevel** - a floor that owns a set of spots.
- **ParkingLot** - owns the levels and runs the show.
- **Ticket** - issued at entry, carries the spot, the vehicle, and the entry time.
- **EntryGate** / **ExitGate** - where cars come and go.

Then the behaviors that *vary* become interfaces, because those are the parts a second lot would want to change without touching the rest:

- **Which free spot a car gets** varies (nearest to the gate, smallest that fits, EV-first). That is a `SpotAssignmentStrategy`.
- **How a stay is priced** varies (flat, per-hour, surge). That is a `PricingStrategy`.

This split - stable nouns as concrete classes, varying behavior behind interfaces - is the Strategy pattern (define a family of interchangeable algorithms behind one interface) plus the Dependency Inversion Principle (the controller depends on the interface, not on any one algorithm). Both are covered in depth in the [SOLID post](/solid-principles-in-detail), so I will apply them rather than re-explain them.

### The structure

Here is the physical half of the model - the entities you would draw on the whiteboard first. These are mostly thin data holders; the only one with interesting behavior is `ParkingSpot`, and we will get to why.

![Parking lot structure class diagram: ParkingLot composes ParkingLevel, which composes ParkingSpot; ParkingSpot aggregates a Vehicle and references SpotType; Vehicle is abstract with Motorcycle, Car and Truck subclasses; Ticket references a ParkingSpot and a Vehicle.](https://mermaid.ink/img/Y2xhc3NEaWFncmFtCiAgICBjbGFzcyBWZWhpY2xlVHlwZSB7CiAgICAgICAgPDxlbnVtZXJhdGlvbj4+CiAgICAgICAgTU9UT1JDWUNMRQogICAgICAgIENBUgogICAgICAgIFRSVUNLCiAgICB9CiAgICBjbGFzcyBTcG90VHlwZSB7CiAgICAgICAgPDxlbnVtZXJhdGlvbj4+CiAgICAgICAgU01BTEwKICAgICAgICBDT01QQUNUCiAgICAgICAgTEFSR0UKICAgIH0KICAgIGNsYXNzIFZlaGljbGUgewogICAgICAgIDw8YWJzdHJhY3Q+PgogICAgICAgICtwbGF0ZSBzdHJpbmcKICAgICAgICArdHlwZSBWZWhpY2xlVHlwZQogICAgfQogICAgY2xhc3MgTW90b3JjeWNsZQogICAgY2xhc3MgQ2FyCiAgICBjbGFzcyBUcnVjawogICAgY2xhc3MgUGFya2luZ1Nwb3QgewogICAgICAgICtpZCBzdHJpbmcKICAgICAgICArdHlwZSBTcG90VHlwZQogICAgICAgIC12ZWhpY2xlIFZlaGljbGUKICAgICAgICArdHJ5UmVzZXJ2ZSh2IFZlaGljbGUpIGJvb2xlYW4KICAgICAgICArcmVsZWFzZSgpIHZvaWQKICAgICAgICAraXNGcmVlKCkgYm9vbGVhbgogICAgfQogICAgY2xhc3MgUGFya2luZ0xldmVsIHsKICAgICAgICArZmxvb3IgbnVtYmVyCiAgICAgICAgK3Nwb3RzIFBhcmtpbmdTcG90W10KICAgIH0KICAgIGNsYXNzIFBhcmtpbmdMb3QgewogICAgICAgICtwYXJrKHYgVmVoaWNsZSkgVGlja2V0CiAgICAgICAgK3VucGFyayh0IFRpY2tldCkgbnVtYmVyCiAgICB9CiAgICBjbGFzcyBUaWNrZXQgewogICAgICAgICtpZCBzdHJpbmcKICAgICAgICArZW50cnlUaW1lIG51bWJlcgogICAgfQogICAgVmVoaWNsZSA8fC0tIE1vdG9yY3ljbGUKICAgIFZlaGljbGUgPHwtLSBDYXIKICAgIFZlaGljbGUgPHwtLSBUcnVjawogICAgVmVoaWNsZSAtLT4gVmVoaWNsZVR5cGUKICAgIFBhcmtpbmdTcG90IC0tPiBTcG90VHlwZQogICAgUGFya2luZ1Nwb3Qgby0tIFZlaGljbGUKICAgIFBhcmtpbmdMZXZlbCAqLS0gUGFya2luZ1Nwb3QKICAgIFBhcmtpbmdMb3QgKi0tIFBhcmtpbmdMZXZlbAogICAgVGlja2V0IC0tPiBQYXJraW5nU3BvdAogICAgVGlja2V0IC0tPiBWZWhpY2xlCg==?type=png&bgColor=FFFFFF)

The filled diamonds are composition (a `ParkingLevel` owns its `ParkingSpot`s and they do not exist without it); the hollow diamond from `ParkingSpot` to `Vehicle` is aggregation (a spot *holds* a vehicle, but the vehicle has a life of its own); the hollow triangles are inheritance. In code, the structure is almost boring - which is the point. It is small, so write it fast and spend your minutes on the part that is not boring.

```typescript
abstract class Vehicle {
  constructor(
    public readonly plate: string,
    public readonly type: VehicleType,
  ) {}
}
class Car extends Vehicle {
  constructor(plate: string) {
    super(plate, VehicleType.Car);
  }
}
// Motorcycle and Truck are identical with their own VehicleType.

class ParkingLevel {
  constructor(
    public readonly floor: number,
    public readonly spots: ParkingSpot[],
  ) {}
}
```

That is the whole structural layer - a couple of enums, three one-line vehicle subclasses, and two holder classes. Do not gold-plate it.

### The control

The interesting half is who talks to whom at runtime: the gates, the controller, and the two strategy interfaces it depends on.

![Parking lot control class diagram: EntryGate and ExitGate associate to ParkingLot; ParkingLot depends on the SpotAssignmentStrategy and PricingStrategy interfaces and on ParkingSpot; BestFitStrategy realizes SpotAssignmentStrategy and HourlyPricing realizes PricingStrategy.](https://mermaid.ink/img/Y2xhc3NEaWFncmFtCiAgICBjbGFzcyBQYXJraW5nTG90IHsKICAgICAgICAtbGV2ZWxzIFBhcmtpbmdMZXZlbFtdCiAgICAgICAgK3BhcmsodiBWZWhpY2xlKSBUaWNrZXQKICAgICAgICArdW5wYXJrKHQgVGlja2V0KSBudW1iZXIKICAgICAgICArZnJlZUNvdW50KHR5cGUgU3BvdFR5cGUpIG51bWJlcgogICAgfQogICAgY2xhc3MgU3BvdEFzc2lnbm1lbnRTdHJhdGVneSB7CiAgICAgICAgPDxpbnRlcmZhY2U+PgogICAgICAgICtmaW5kU3BvdChsZXZlbHMsIHYpIFBhcmtpbmdTcG90CiAgICB9CiAgICBjbGFzcyBCZXN0Rml0U3RyYXRlZ3kgewogICAgICAgICtmaW5kU3BvdChsZXZlbHMsIHYpIFBhcmtpbmdTcG90CiAgICB9CiAgICBjbGFzcyBQcmljaW5nU3RyYXRlZ3kgewogICAgICAgIDw8aW50ZXJmYWNlPj4KICAgICAgICArcHJpY2UodGlja2V0LCBleGl0VGltZSkgbnVtYmVyCiAgICB9CiAgICBjbGFzcyBIb3VybHlQcmljaW5nIHsKICAgICAgICArcHJpY2UodGlja2V0LCBleGl0VGltZSkgbnVtYmVyCiAgICB9CiAgICBjbGFzcyBFbnRyeUdhdGUgewogICAgICAgICtpZCBzdHJpbmcKICAgICAgICArYWRtaXQodiBWZWhpY2xlKSBUaWNrZXQKICAgIH0KICAgIGNsYXNzIEV4aXRHYXRlIHsKICAgICAgICAraWQgc3RyaW5nCiAgICAgICAgK2NoZWNrb3V0KHQgVGlja2V0KSBudW1iZXIKICAgIH0KICAgIGNsYXNzIFBhcmtpbmdTcG90IHsKICAgICAgICArdHJ5UmVzZXJ2ZSh2IFZlaGljbGUpIGJvb2xlYW4KICAgIH0KICAgIFNwb3RBc3NpZ25tZW50U3RyYXRlZ3kgPHwuLiBCZXN0Rml0U3RyYXRlZ3kKICAgIFByaWNpbmdTdHJhdGVneSA8fC4uIEhvdXJseVByaWNpbmcKICAgIFBhcmtpbmdMb3QgLi4+IFNwb3RBc3NpZ25tZW50U3RyYXRlZ3kKICAgIFBhcmtpbmdMb3QgLi4+IFByaWNpbmdTdHJhdGVneQogICAgUGFya2luZ0xvdCAuLj4gUGFya2luZ1Nwb3QKICAgIEVudHJ5R2F0ZSAtLT4gUGFya2luZ0xvdAogICAgRXhpdEdhdGUgLS0+IFBhcmtpbmdMb3QK?type=png&bgColor=FFFFFF)

The dashed arrows from `ParkingLot` to the two interfaces are dependencies on abstractions, not on `BestFitStrategy` or `HourlyPricing` directly. That is the whole reason a second lot can swap in a different rule without editing the controller.

## The one piece of code that carries risk

Everything above is warm-up. The line that actually decides whether this design is correct is the spot claim, because of the airport detail: many gates admit cars at once. If "find a free spot" and "mark it taken" are two separate steps, two gates can both find spot `C1` free and both park a car in it.

The fix is to make the claim a single atomic step - a *critical section* (a piece of code that only one actor may be inside at a time) that flips a spot from free to taken and tells the caller whether they won. `ParkingSpot.tryReserve` is that step:

```typescript
class ParkingSpot {
  private vehicle: Vehicle | null = null;

  // Atomic claim: returns true only for the caller that flips it from free to taken.
  // In one Node thread this is trivially atomic; the point is that this is the ONE
  // critical section every entry gate races on. In a real multi-node lot it becomes a
  // DB compare-and-set (UPDATE ... WHERE occupied = false) or SELECT ... FOR UPDATE.
  tryReserve(vehicle: Vehicle): boolean {
    if (this.vehicle !== null) return false;
    this.vehicle = vehicle;
    return true;
  }

  release(): void {
    this.vehicle = null;
  }
  isFree(): boolean {
    return this.vehicle === null;
  }
}
```

The controller uses it in a claim-or-retry loop. It asks the strategy for a candidate spot, tries to reserve it, and if another gate got there first, loops and asks for the next one. This is optimistic concurrency: assume you will win the race, and only redo the work on the rare occasion you lose it.

```typescript
class ParkingLot {
  constructor(
    private readonly levels: ParkingLevel[],
    private readonly assignment: SpotAssignmentStrategy,
    private readonly pricing: PricingStrategy,
  ) {}

  park(vehicle: Vehicle, now: number = Date.now()): Ticket {
    while (true) {
      const spot = this.assignment.findSpot(this.levels, vehicle);
      if (spot === null) throw new Error(`Lot full for ${vehicle.type}`);
      if (spot.tryReserve(vehicle)) {
        return new Ticket(`T-${++this.ticketSeq}`, spot, vehicle, now);
      }
      // Lost the race to another gate; loop and pick the next free spot.
    }
  }

  unpark(ticket: Ticket, exitTime: number = Date.now()): number {
    const fee = this.pricing.price(ticket, exitTime);
    ticket.spot.release();
    return fee;
  }

  private ticketSeq = 0;
}
```

Being explicit about this is what separates a passing answer from a strong one. Most candidates write `spot.occupied = true` and never mention that two gates can collide. Naming the critical section, and pointing out that in a real distributed lot the same idea becomes a database `compare-and-set` (write only if the row still shows the value you read) or a `SELECT ... FOR UPDATE` row lock, is the sentence that lands.

## The two pluggable policies

With the risky part done, the two strategies are short and show the extensibility you promised. Spot assignment picks the smallest free spot that still fits, so a motorcycle does not sit in a truck bay while bikes queue outside:

```typescript
class BestFitStrategy implements SpotAssignmentStrategy {
  // A vehicle can use its own size or larger; list them smallest-first to avoid waste.
  private static readonly FITS: Record<VehicleType, SpotType[]> = {
    [VehicleType.Motorcycle]: [SpotType.Small, SpotType.Compact, SpotType.Large],
    [VehicleType.Car]: [SpotType.Compact, SpotType.Large],
    [VehicleType.Truck]: [SpotType.Large],
  };

  findSpot(levels: ParkingLevel[], vehicle: Vehicle): ParkingSpot | null {
    for (const type of BestFitStrategy.FITS[vehicle.type]) {
      for (const level of levels) {
        for (const spot of level.spots) {
          if (spot.type === type && spot.isFree()) return spot;
        }
      }
    }
    return null;
  }
}
```

Pricing rounds the stay up to the hour, rates it by vehicle type, and caps it per day. Swapping to weekend surge or a free grace period is a new class implementing the same `price` method, with nothing in `ParkingLot` changing:

```typescript
class HourlyPricing implements PricingStrategy {
  private static readonly RATE_PER_HOUR: Record<VehicleType, number> = {
    [VehicleType.Motorcycle]: 20,
    [VehicleType.Car]: 40,
    [VehicleType.Truck]: 60,
  };
  private static readonly DAY_CAP: Record<VehicleType, number> = {
    [VehicleType.Motorcycle]: 200,
    [VehicleType.Car]: 400,
    [VehicleType.Truck]: 600,
  };

  price(ticket: Ticket, exitTime: number): number {
    const ms = Math.max(0, exitTime - ticket.entryTime);
    const hours = Math.max(1, Math.ceil(ms / 3_600_000)); // round up, minimum one hour
    const t = ticket.vehicle.type;
    return Math.min(hours * HourlyPricing.RATE_PER_HOUR[t], HourlyPricing.DAY_CAP[t]);
  }
}
```

The gates are three lines each - `admit` forwards to `park`, `checkout` forwards to `unpark` - so I will leave them to the [full source in the repo](https://github.com/SankalpSTG/system-design-guide/tree/main/lld/parking-lot). Running the demo there shows the race resolve: two cars arrive at two gates for the last compact spot, one wins it, and the other spills to a large spot instead of double-booking.

## Where the airport comes back

Now you can honour the scale question you deferred. If the interviewer pushes on 20,000 spots and distributed gates, you have a clean place to point, because the object model does not have to change - only where the state lives does:

- **Spot lookup at scale.** The triple `for` loop is fine for one lot in memory. For 20,000 spots, keep a free-list or a free-count per spot type per level so `findSpot` is O(1) to O(levels) instead of a full scan. The `SpotAssignmentStrategy` interface does not change; only its implementation does.
- **The claim, distributed.** `tryReserve` is already the seam. Move the spot's `occupied` flag into a row in a database and the atomic flip becomes `UPDATE spots SET occupied = true WHERE id = ? AND occupied = false` - it updates zero rows if someone beat you, which is exactly the `false` return you already handle. The retry loop stays as written.
- **Sharding.** Partition spots by level or building across nodes; each gate talks to the shard for the building it serves. This is a data-layer concern that sits *under* `ParkingLot`, which is why keeping it out of the object model was the right call in the room.

That is the payoff of scoping down on purpose at the start: the small design you actually built extends cleanly to the big one you were teased with, and you can say so in one breath.

## What the interviewer is grading

A machine-coding round wants clean, extensible, working code inside the hour, and a candidate who spends their minutes where the risk is. For this problem that means: you asked how big before you drew anything, you turned nouns into classes and *varying behavior* into strategies, you found the one line that concurrency can break and made it atomic, and you named the distributed version without drowning in it. The class diagram is your artifact; the atomic claim is your proof you understood the problem. Everything else is holders and glue you can write quickly.

Next in this series we will take on a problem where the object's *behavior* changes with its state rather than its concurrency - a vending machine, modelled with the State pattern - and see when a `switch` on a status field should become a set of state classes instead.

