Skip to main content
Low-Level Design · lesson 5 of 18
Open the Low-Level Design page →
Low-Level Design intermediate Lesson 5 of 18

Design a Parking Lot: Allocating and Freeing Spots

A motorcycle, a car, and a bus compete for three spot sizes — best-fit allocation, a full-lot rejection, and freeing a spot back for reuse.

By SkillByExample · Published

lld oop python parking-lot

Three vehicle types, three spot sizes, and a rule about which vehicle fits which spot — the first real “design a system” problem in this track.

Spots, vehicles, and best-fit allocation

class ParkingSpot:
    def __init__(self, spot_id, size):
        self.spot_id = spot_id
        self.size = size  # "small", "medium", "large"
        self.vehicle = None

    @property
    def is_free(self):
        return self.vehicle is None

SIZE_RANK = {"small": 1, "medium": 2, "large": 3}
VEHICLE_MIN_SIZE = {"motorcycle": "small", "car": "medium", "bus": "large"}

class ParkingLot:
    def __init__(self, spots):
        self.spots = spots

    def allocate(self, vehicle_id, vehicle_type):
        min_size = VEHICLE_MIN_SIZE[vehicle_type]
        candidates = [
            s for s in self.spots
            if s.is_free and SIZE_RANK[s.size] >= SIZE_RANK[min_size]
        ]
        if not candidates:
            print(f"No spot available for {vehicle_type} {vehicle_id}")
            return None
        best = min(candidates, key=lambda s: SIZE_RANK[s.size])
        best.vehicle = vehicle_id
        print(f"{vehicle_id} ({vehicle_type}) parked at spot {best.spot_id} ({best.size})")
        return best.spot_id

    def free(self, spot_id):
        for s in self.spots:
            if s.spot_id == spot_id:
                if s.is_free:
                    print(f"Spot {spot_id} is already free")
                    return
                print(f"Spot {spot_id} freed (was {s.vehicle})")
                s.vehicle = None
                return
        print(f"No such spot {spot_id}")

lot = ParkingLot([
    ParkingSpot(1, "small"),
    ParkingSpot(2, "medium"),
    ParkingSpot(3, "large"),
])

lot.allocate("bike-1", "motorcycle")
lot.allocate("car-1", "car")
lot.allocate("bus-1", "bus")
lot.allocate("car-2", "car")
lot.free(2)
lot.allocate("car-2", "car")
$ python parking_lot.py
bike-1 (motorcycle) parked at spot 1 (small)
car-1 (car) parked at spot 2 (medium)
bus-1 (bus) parked at spot 3 (large)
No spot available for car car-2
Spot 2 freed (was car-1)
car-2 (car) parked at spot 2 (medium)

car-2’s first attempt fails outright — every spot compatible with a car (medium or large) is already taken, and the lot correctly refuses rather than bumping anyone. Freeing spot 2 makes it available again, and the next identical allocation call succeeds this time.

Practice

1. Confirm a motorcycle falls back to a large spot when nothing smaller is free.
class ParkingSpot:
    def __init__(self, spot_id, size):
        self.spot_id = spot_id
        self.size = size
        self.vehicle = None
    @property
    def is_free(self):
        return self.vehicle is None

SIZE_RANK = {"small": 1, "medium": 2, "large": 3}
VEHICLE_MIN_SIZE = {"motorcycle": "small", "car": "medium", "bus": "large"}

class ParkingLot:
    def __init__(self, spots):
        self.spots = spots
    def allocate(self, vehicle_id, vehicle_type):
        min_size = VEHICLE_MIN_SIZE[vehicle_type]
        candidates = [s for s in self.spots if s.is_free and SIZE_RANK[s.size] >= SIZE_RANK[min_size]]
        if not candidates:
            print(f"No spot available for {vehicle_type} {vehicle_id}")
            return None
        best = min(candidates, key=lambda s: SIZE_RANK[s.size])
        best.vehicle = vehicle_id
        print(f"{vehicle_id} ({vehicle_type}) parked at spot {best.spot_id} ({best.size})")
        return best.spot_id

lot = ParkingLot([ParkingSpot(1, "large")])
lot.allocate("bike-1", "motorcycle")
$ python fallback.py
bike-1 (motorcycle) parked at spot 1 (large)

SIZE_RANK[s.size] >= SIZE_RANK[min_size] accepts any spot at least as big as required, so a motorcycle (needs small) matches a large spot fine when it’s the only one left.

2. Add an available_count method reporting free spots by size.
class ParkingSpot:
    def __init__(self, spot_id, size):
        self.spot_id = spot_id
        self.size = size
        self.vehicle = None
    @property
    def is_free(self):
        return self.vehicle is None

class ParkingLot:
    def __init__(self, spots):
        self.spots = spots
    def available_count(self):
        counts = {}
        for s in self.spots:
            if s.is_free:
                counts[s.size] = counts.get(s.size, 0) + 1
        return counts

lot = ParkingLot([
    ParkingSpot(1, "small"), ParkingSpot(2, "small"),
    ParkingSpot(3, "medium"), ParkingSpot(4, "large"),
])
lot.spots[0].vehicle = "bike-1"
print(f"Available: {lot.available_count()}")
$ python available_count.py
Available: {'small': 1, 'medium': 1, 'large': 1}

Only one small spot shows as available — the other was occupied directly for this example — confirming the count reflects live is_free state, not the lot’s total capacity.

3. Confirm freeing a spot ID that doesn't exist fails cleanly.
class ParkingSpot:
    def __init__(self, spot_id, size):
        self.spot_id = spot_id
        self.size = size
        self.vehicle = None
    @property
    def is_free(self):
        return self.vehicle is None

class ParkingLot:
    def __init__(self, spots):
        self.spots = spots
    def free(self, spot_id):
        for s in self.spots:
            if s.spot_id == spot_id:
                if s.is_free:
                    print(f"Spot {spot_id} is already free")
                    return
                s.vehicle = None
                print(f"Spot {spot_id} freed")
                return
        print(f"No such spot {spot_id}")

lot = ParkingLot([ParkingSpot(1, "small")])
lot.free(99)
$ python bad_free.py
No such spot 99

The for loop falls through to the trailing print when no spot matches — there’s no IndexError or silent no-op, because free never assumes the spot ID it’s given is valid.

Next: an elevator that has to schedule floor requests, not just move up and down on command.

Frequently Asked Questions

Why does a motorcycle get the small spot instead of just taking any free spot?
The allocator picks the smallest spot that's still big enough (`min(candidates, key=lambda s: SIZE_RANK[s.size])`) — this is a best-fit strategy, which matters in a real lot because it keeps larger spots free for vehicles that actually need them. A first-fit strategy (just take the first free spot found) would risk a bus arriving to find every large spot taken by cars that could have used a medium one instead.
What real-world LLD interview question does this map to?
"Design a parking lot" is one of the most commonly asked machine-coding interview questions, specifically because it tests whether you handle multiple entity types (spot sizes, vehicle types) and their compatibility rules cleanly, rather than because the domain itself is complex. The compatibility rule here (`SIZE_RANK[s.size] >= SIZE_RANK[min_size]`) is the crux most solutions get right or wrong.
Why is is_free a property instead of a plain method call like is_free()?
`@property` lets `s.is_free` read like an attribute (a yes/no fact about the spot) rather than an action being performed (`s.is_free()` would suggest it does something). Python convention reserves properties for cheap, side-effect-free computations exactly like this one — checking whether `self.vehicle is None`.
What happens if two vehicles try to grab the same spot at the exact same time?
This lesson's allocate() has no protection against that — it's a single-threaded example. The concurrency lesson later in this track builds exactly this race condition into a parking lot on purpose, shows the double-booking bug with real output, and then fixes it with a lock.