Design a parking lot
Picture
A vehicle arrives, claims a fitting free spot, and receives a ticket. On exit, that exact ticket releases that exact stay. Begin with the two invariants: one active vehicle per spot, one active ticket per vehicle.
Predict
What must remain unchanged if no fitting spot is available?
Walk through the small example
All spots and active tickets. A failed allocation should not issue a ticket or partially claim resources.
Mental model and complete Python solution
Spot holds fit and occupancy data; immutable Ticket identifies a stay; ParkingLot coordinates them. The policy chooses the smallest fitting free spot and breaks ties by spot ID. Track both tickets and vehicles to reject duplicate active parking. Exit removes a ticket once; an old ticket cannot later release a new stay in the same spot. This is a single-threaded simulator; a concurrent implementation must make validation and claiming atomic.
What the function promises: Vehicle and spot sizes are 1=small, 2=medium, 3=large. A larger spot can fit a smaller vehicle. park returns Ticket or None if full; duplicate active vehicles raise. leave returns whether an active ticket was closed. Payment, reservations, databases, and multiple gates are intentionally out of scope.
from dataclasses import dataclass
from typing import Optional
@dataclass
class Spot:
id: int
size: int
vehicle: Optional[str] = None
def fits(self, size):
return self.vehicle is None and size <= self.size
@dataclass(frozen=True)
class Ticket:
id: int
vehicle: str
spot: int
class ParkingLot:
def __init__(self, spots):
spots = list(spots)
if len({s.id for s in spots}) != len(spots) or any(
s.size not in (1,2,3) or s.vehicle is not None for s in spots):
raise ValueError("Need unique, initially free, valid spots")
self.spots = {s.id: Spot(s.id, s.size) for s in spots}
self.tickets, self.vehicles = {}, {}
self.next_ticket = 1
def park(self, vehicle, size):
if size not in (1,2,3) or not vehicle:
raise ValueError("Invalid vehicle")
if vehicle in self.vehicles:
raise ValueError("Vehicle is already parked")
candidates = [s for s in self.spots.values() if s.fits(size)]
if not candidates:
return None
spot = min(candidates, key=lambda s: (s.size, s.id))
ticket = Ticket(self.next_ticket, vehicle, spot.id)
self.next_ticket += 1
spot.vehicle = vehicle
self.tickets[ticket.id] = ticket
self.vehicles[vehicle] = ticket.id
return ticket
def leave(self, ticket_id):
ticket = self.tickets.pop(ticket_id, None)
if ticket is None:
return False
self.spots[ticket.spot].vehicle = None
del self.vehicles[ticket.vehicle]
return True
lot = ParkingLot([Spot(1, 1), Spot(2, 3)])
a = lot.park("bike", 1)
b = lot.park("car", 2)
assert (a.spot, b.spot) == (1, 2)
assert lot.park("truck", 3) is None
assert lot.leave(b.id)
c = lot.park("truck", 3)
assert not lot.leave(b.id) and lot.spots[c.spot].vehicle == "truck"
Work and storage: O(S) allocation over S spots and expected O(1) exit lookups. Storage is O(S+active tickets). A free-spot index is an optimization to introduce only if these scans matter.
When to recognize it: Start with one vehicle per spot and one active ticket per vehicle. Let one service coordinate both facts.
Check before applying it: Multiple gates require an atomic allocation boundary. Payment, reservations, and durable recovery are outside this first in-memory design.
Change one rule
Add reservations and multiple entrance gates. Which boundary becomes more important than class naming?
See what changes
Atomic allocation across available, reserved, and occupied states. A shared database transaction or equivalent conditional claim must ensure only one caller wins; a check in one process alone is insufficient.
Connect
Objects and responsibilities: Spots and tickets have different responsibilities.
Make failure and retries part of the design: Protect check-and-claim when concurrent requests arrive.