Designing a parking lot

A classic object-design question: model the lot, the spots, the cars, and the rules that decide who parks where and what they owe.

The idea

A parking lot is a tidy little world of objects. A ParkingLot holds one or more Levels; each level holds many Spots, and every spot has a size — motorcycle, compact, large, or handicapped. A Vehicle arriving at the gate needs the nearest free spot it actually fits into.

When a car parks, the system issues a Ticket recording the spot and the entry time. On the way out, the ticket's duration sets the fee. The whole interview is really about three things: the entities and how they relate, the policy for assigning a spot, and doing it safely when many cars arrive at once.

Press play, or step through one move at a time.

How it works

Each level keeps its spots in order so it can scan for the first free spot whose size fits the vehicle. A motorcycle fits anywhere; a large vehicle needs a large (or handicapped) spot. The assignment runs under a lock so two cars can never claim the same spot.

SIZE_RANK = {"motorcycle": 0, "compact": 1, "large": 2}

class Vehicle:
    def __init__(self, size):
        self.size = size            # "motorcycle" | "compact" | "large"

class ParkingSpot:
    def __init__(self, spot_id, size):
        self.id = spot_id
        self.size = size
        self.is_free = True

    def fits(self, vehicle):
        # a vehicle fits a spot at least as big as itself
        return SIZE_RANK[vehicle.size] <= SIZE_RANK[self.size]

class Level:
    def __init__(self, spots):
        self.spots = spots          # ordered nearest -> farthest

    def find_spot(self, vehicle):
        for spot in self.spots:     # first-fit, nearest first
            if spot.is_free and spot.fits(vehicle):
                return spot
        return None

class Ticket:
    def __init__(self, spot, entry_time):
        self.spot = spot
        self.entry_time = entry_time

class ParkingLot:
    def __init__(self, levels, rate=3):
        self.levels = levels
        self.rate = rate            # dollars per hour
        self.lock = Lock()
        self.tickets = {}

    def park(self, vehicle, now):
        with self.lock:             # atomic: no double-booking
            for level in self.levels:
                spot = level.find_spot(vehicle)
                if spot:
                    spot.is_free = False
                    t = Ticket(spot, now)
                    self.tickets[id(t)] = t
                    return t
        return None                 # lot full for this size

    def leave(self, ticket, now):
        hours = ceil((now - ticket.entry_time) / 3600)
        fee = max(1, hours) * self.rate
        ticket.spot.is_free = True
        return fee

The find_spot loop is first-fit: because spots are stored nearest-first, the first match is also the nearest. The fee rounds partial hours up with ceil, so 2.5 hours bills as 3.

Trade-offs

DecisionOptionWhy you'd pick it
Assignment policyFirst-fit (nearest)Simple and fast; spots stored in walking order, so the first match is also the closest.
Assignment policyBest-fit (smallest that fits)Keeps large spots free for large cars, but costs a full scan and can strand cars far away.
ConcurrencySingle global lockTrivially correct, but every gate serializes through one lock — a throughput ceiling.
ConcurrencyPer-level lock or atomic spot flagFar more parallelism; harder to reason about and to keep counters consistent.
Occupancy storeIn-memory mapMicrosecond reads, dead simple — but state is lost on restart and can't scale past one node.
Occupancy storeDatabase row per spotDurable and shareable across nodes; needs transactions and pays network latency per park.

Watch out for

Worked example

Take a small lot: 4 compact spots (C1–C4) and 2 large spots (L1–L2), all free, rate $3/hour. Four vehicles arrive in order.

Motorcycle — first-fit scans C1 first; a motorcycle fits a compact spot, so it parks at C1 and gets a ticket. Car A (compact) takes the next free fitting spot, C2. Car B (compact) takes C3. Truck (large) needs a large spot — compacts don't fit — so the scan skips C1–C4 and parks it at L1.

Now Car A leaves after 2.5 hours. The fee rounds the duration up: ceil(2.5) = 3 hours, so 3 × $3 = $9. Spot C2 flips back to free, and the next compact arrival can reuse it. The rounding rule is the thing to say out loud: any started hour is a full billed hour.

Check yourself

A large truck arrives and the only free spots left are two compact ones. What should park do?

Two cars hit the gate at the same instant and both see spot C1 free. What keeps them from both parking there?