Skip to content
System design course
Ch.4 · Designing real systems·how to build it ·9 min read

Building a parking garage

Turn the entity model into working classes — the spot-allocation algorithm, duration-based pricing via Strategy, and thread-safe concurrent parking.


From model to code

The design lesson gave us entities and patterns. Now implement the core: the classes, the allocation algorithm, pricing via Strategy, and thread safety for concurrent entries/exits. (Python-flavored pseudocode; the structure is what matters.)

The enums and the spot

from enum import Enum

class VehicleType(Enum): MOTORCYCLE=1; CAR=2; TRUCK=3
class SpotType(Enum):    MOTORCYCLE=1; COMPACT=2; LARGE=3

# which spot types each vehicle may use (smallest that fits, and up)
FITS = {
    VehicleType.MOTORCYCLE: [SpotType.MOTORCYCLE, SpotType.COMPACT, SpotType.LARGE],
    VehicleType.CAR:        [SpotType.COMPACT, SpotType.LARGE],
    VehicleType.TRUCK:      [SpotType.LARGE],
}

class ParkingSpot:
    def __init__(self, spot_id, spot_type):
        self.id, self.type, self.vehicle = spot_id, spot_type, None
    def is_free(self): return self.vehicle is None

Allocation: free spots per type, per level

The naive approach scans every spot — O(spots) per car. Instead, each Level keeps a set/queue of free spots bucketed by type, so finding a spot is O(1):

from collections import defaultdict

class Level:
    def __init__(self, level_id, spots):
        self.id = level_id
        self.free = defaultdict(set)          # SpotType -> {spot_id}
        self.spots = {}
        for s in spots:
            self.spots[s.id] = s
            self.free[s.type].add(s.id)

    def assign(self, vehicle):                # returns a spot or None
        for st in FITS[vehicle.type]:         # try smallest fitting type first
            if self.free[st]:
                spot = self.spots[self.free[st].pop()]
                spot.vehicle = vehicle
                return spot
        return None

    def release(self, spot):
        spot.vehicle = None
        self.free[spot.type].add(spot.id)

Trying the smallest fitting type first keeps large spots open for vehicles that truly need them — a simple, sensible allocation policy (swap in a Strategy if rules get complex).

Pricing via the Strategy pattern

Pricing is a pluggable policy so new schemes don’t touch the lot:

class PricingStrategy:                         # interface
    def price(self, entry, exit): ...

class HourlyRate(PricingStrategy):
    def __init__(self, rate): self.rate = rate
    def price(self, entry, exit):
        hours = ceil((exit - entry).total_seconds() / 3600)
        return hours * self.rate

class TieredRate(PricingStrategy):             # e.g. first hour cheap, then more
    def price(self, entry, exit): ...

The exit panel just calls strategy.price(ticket.entry_time, now) — it neither knows nor cares which scheme is active.

The top-level flow

class ParkingLot:
    def __init__(self, levels, pricing):
        self.levels, self.pricing = levels, pricing
        self.lock = threading.Lock()

    def park(self, vehicle):
        with self.lock:                        # thread-safe assignment
            for level in self.levels:
                spot = level.assign(vehicle)
                if spot:
                    return Ticket(vehicle, spot, level, now())
            raise GarageFullError()

    def unpark(self, ticket):
        with self.lock:
            ticket.level.release(ticket.spot)
        fee = self.pricing.price(ticket.entry_time, now())
        return fee

Thread safety: the real correctness bug

Two cars entering at once can be handed the same last spot if assignment isn’t atomic — a classic race. The fix is to guard the find-and-occupy as one critical section (the lock above), or use atomic operations on the free-spot structure. Always raise this in the interview: “parking is a read-modify-write on shared state, so I serialize the assign/release with a lock (or per-level locks to reduce contention).” Per-level locks scale better than one global lock.

Edge cases to mention

  • Garage full for a vehicle’s type (large free, but a truck needs large).
  • Lost ticket (charge a max/day rate).
  • Vehicle occupying multiple spots (a bus needs N adjacent large spots — extend assign to reserve a contiguous run).
  • Re-entrancy / double exit on the same ticket.

The takeaway

The OOD signal is: clean classes, an O(1) bucketed allocation instead of a scan, Strategy for pricing so it’s extensible, and explicitly locking the shared assignment to avoid the double-allocation race. That’s a complete, defensible implementation — and the same modeling discipline scaffolds every larger system in this chapter.