Skip to content
BytePatterns

Design a Parking Lot: The Low-Level Design Interview Answer

8 min readBytePatterns

Design a parking lot in a low-level design interview: requirements first, then Lot, Spot and Ticket, pluggable allocation and pricing, in runnable Python code.

"Design a parking lot" is the classic low-level design warm-up, and it is easy to answer badly. The weak answer opens with a class diagram of fifteen classes, three levels of inheritance and a ParkingLotManagerFactory. The strong answer asks a few questions, names five objects, gives each one job, and then spends its time on the one real design point: which parts of the system will change, and how to let them change without rewriting the rest.

The problem it solves

The interviewer wants to see how you turn a vague request into objects with clear responsibilities. Start with requirements, because they decide which objects exist:

  • Vehicle classes and spot sizes. Say small, medium and large, where a spot fits its own size and anything smaller.
  • Capacity. A fixed set of spots, possibly across floors.
  • Entry and exit. A vehicle gets a spot and a ticket on entry; on exit the ticket is priced and the spot is freed.
  • Pricing. Per started hour, by spot size, for example.
  • What happens when full. The vehicle is turned away, and nothing changes.

Out of scope unless asked: payments, reservations, displays, and a database. Say so explicitly; scoping is part of the answer.

The intuition

Name the objects first: Lot, Spot, Vehicle, Ticket, and a pricing rule. Then give each one job:

  • Spot knows its size and whether it is occupied.
  • Ticket ties a plate to a spot and an entry time. It holds everything exit will need, so exit never has to search.
  • Lot only matches and counts: it asks for a spot, records the ticket, and frees the spot on exit.

The design point is what changes. Spot allocation rules change often: smallest spot that fits, nearest the entrance, reserved spaces for electric vehicles. Pricing changes too: weekend rates, a first-hour discount. If those rules live as if/elif chains inside Lot, every business change edits the core class. Instead, give each its own seam: an allocation strategy and a pricing rule, objects the lot calls through a tiny interface. That is the strategy pattern, applied where change actually happens rather than everywhere.

The last thing to mention is concurrency. With two entry gates, two cars can be offered the same free spot. Choosing and occupying a spot must be one atomic step, so the lot holds a lock around it; the single-threaded code below leaves it out.

Watch it run

The animation starts by naming the objects before the classes: Lot, Spot, Vehicle, Ticket, and a pricing rule. Then one job each: the spot knows its size; the lot only matches and counts. A small car, AB12, arrives at the barrier and asks to park. The lot checks the free count for that vehicle class. One is free, so it is taken and the count drops. The ticket ties plate to spot and entry time, everything exit will need. The next small car, CD34, finds the class full and is turned away with no side effects: no ticket, no count touched. On exit the ticket, not the driver, says which spot to free. The count goes back up, and the lot forgets the car entirely. Then the design question: which part changes most? Allocation does, weekly. So it is a pluggable rule the lot calls, not an if/elif buried inside it. Pricing moves the same way, for the same reason: it is the other part that keeps moving.

Designing a Parking Lot

Step 1 of 13

Name the objects before the classes: Lot, Spot, Vehicle, Ticket, and a pricing rule.

The same interactive animation as the lesson — step through it with the controls.

The code

The lesson's Lot tracks only free counts. A fuller version keeps real spots, a ticket with an entry time, and the two pluggable parts. Times are minutes passed in by the caller, so the code is testable without a clock:

from dataclasses import dataclass
from math import ceil

SIZES = ["small", "medium", "large"]         # a spot fits its own size and anything smaller

def fits(spot_size, vehicle_size):
    return SIZES.index(spot_size) >= SIZES.index(vehicle_size)

@dataclass
class Spot:
    id: str
    size: str
    plate: str = ""                          # empty means free

@dataclass
class Ticket:
    plate: str
    spot: Spot
    entered: int                             # minutes since opening

class SmallestFit:                           # allocation: the part that changes
    def choose(self, spots, size):
        free = [s for s in spots if not s.plate and fits(s.size, size)]
        return min(free, key=lambda s: (SIZES.index(s.size), s.id), default=None)

class HourlyPricing:                         # pricing: the other part that changes
    def __init__(self, rates):
        self.rates = rates
    def fee(self, ticket, now):
        hours = max(1, ceil((now - ticket.entered) / 60))   # started hours, minimum one
        return hours * self.rates[ticket.spot.size]

class Lot:
    def __init__(self, spots, allocator, pricing):
        self.spots, self.allocator, self.pricing = spots, allocator, pricing
        self.tickets = {}                    # plate -> Ticket

    def park(self, plate, size, now):
        if plate in self.tickets:
            return None                      # already inside
        spot = self.allocator.choose(self.spots, size)
        if spot is None:
            return None                      # full for this size: nothing changes
        spot.plate = plate
        self.tickets[plate] = Ticket(plate, spot, now)
        return self.tickets[plate]

    def leave(self, plate, now):
        ticket = self.tickets.pop(plate)     # the ticket says which spot to free
        ticket.spot.plate = ""
        return self.pricing.fee(ticket, now)

    def free(self):
        return {z: sum(1 for s in self.spots if s.size == z and not s.plate) for z in SIZES}

One spot of each size. The second small car overflows into the medium spot, the medium car is turned away, and the first car pays for three started hours:

spots = [Spot("S1", "small"), Spot("M1", "medium"), Spot("L1", "large")]
lot = Lot(spots, SmallestFit(), HourlyPricing({"small": 2, "medium": 3, "large": 5}))
print(lot.park("AB12", "small", now=0).spot.id)    # S1
print(lot.park("CD34", "small", now=5).spot.id)    # M1   small is full, so the next size up
print(lot.park("EF56", "large", now=9).spot.id)    # L1
print(lot.park("GH78", "medium", now=12))          # None
print(lot.free())            # {'small': 0, 'medium': 0, 'large': 0}
print(lot.leave("AB12", now=130), lot.free())      # 6 {'small': 1, 'medium': 0, 'large': 0}

The payoff of the seam: a new allocation rule is a new class, and Lot is untouched:

class LargestFirst:                          # a new rule, and Lot does not change
    def choose(self, spots, size):
        free = [s for s in spots if not s.plate and fits(s.size, size)]
        return max(free, key=lambda s: (SIZES.index(s.size), s.id), default=None)

lot2 = Lot([Spot("S1", "small"), Spot("L1", "large")], LargestFirst(), HourlyPricing({"small": 2, "large": 5}))
print(lot2.park("AB12", "small", now=0).spot.id)   # L1

The lot against a brute-force model that rescans every spot on each arrival, over 1,000 random days of arrivals and departures: the chosen spot is always the smallest free one that fits, a full lot changes nothing, fees match the formula, and no spot ever holds two cars:

import random

random.seed(21)
ok = True
rates = {"small": 2, "medium": 3, "large": 5}
for _ in range(1000):
    spots = [Spot(f"P{i}", random.choice(SIZES)) for i in range(random.randint(1, 6))]
    lot, inside, now = Lot(spots, SmallestFit(), HourlyPricing(rates)), {}, 0
    for _ in range(40):
        now += random.randint(0, 90)
        plate = random.choice("ABCDEFGH")
        if plate in inside and random.random() < 0.5:
            spot_id, size, entered = inside.pop(plate)
            ok &= lot.leave(plate, now) == max(1, -(-(now - entered) // 60)) * rates[size]
        else:
            size = random.choice(SIZES)
            taken = {v[0] for v in inside.values()}
            options = sorted((SIZES.index(s.size), s.id) for s in spots   # brute force: scan every spot
                             if s.id not in taken and SIZES.index(s.size) >= SIZES.index(size))
            t = lot.park(plate, size, now)
            if plate in inside or not options:
                ok &= t is None
            else:
                ok &= t is not None and t.spot.id == options[0][1]
                inside[plate] = (t.spot.id, t.spot.size, now)
        ok &= sorted(s.plate for s in spots if s.plate) == sorted(inside)   # one car per spot
print(ok)                    # True

The complexity

  • Park: O(S) here, scanning S spots. For a large lot, keep a free list or a min-heap per size, and park becomes O(log S) or O(1).
  • Leave: O(1): the ticket is found by plate and already points at its spot.
  • Space: O(S) for spots plus one ticket per parked vehicle.

Where it goes wrong

  • Drawing classes before asking questions. Requirements decide the objects; guessing them invents the wrong ones.
  • Inheritance for vehicle types. A Car and Van subclass with no behaviour of their own is a size field in disguise.
  • Allocation and pricing inside Lot. Every rule change then edits the core class. Make them strategies.
  • Side effects on a failed park. A full lot must not issue a ticket or change a count.
  • Ignoring two gates. Choose-then-occupy must be atomic, or two cars get the same spot, a classic race condition.
  • Not saying which size is billed. Here a small car in a medium spot pays the medium rate; state the rule you picked.

When it shows up in interviews

It is the most common opening question in low-level design rounds, alongside designing an LRU cache. Follow-ups add floors and a display of free counts, electric-vehicle spots, reservations, or multiple gates, and each one tests whether your seams were in the right place. System design rounds ask the same kind of question one level up, as in design a URL shortener.

How to say it in an interview

"First the requirements: vehicle sizes, how spots fit them, what happens when full, and how pricing works. Then the objects: a Spot knows its size and occupant, a Ticket ties plate, spot and entry time, and the Lot only matches and counts. On entry the lot asks an allocation strategy for a spot; if none fits, it rejects the car with no side effects. On exit the ticket tells it which spot to free, and a pricing rule computes the fee. Allocation and pricing are the parts that change, so they are pluggable strategies rather than branches inside the lot. With several gates, choosing and occupying a spot happens under a lock."