The most-asked machine-coding problem in India, and the one where most candidates lose marks for things that have nothing to do with parking.
The parking lot is the "hello world" of the machine-coding rounds that Indian product companies such as Flipkart, Swiggy, PhonePe and Razorpay are known for. It looks easy, and that is the trap. Everyone can write a ParkingLot class. The interviewer wants to see whether you separate the parts that change (pricing, spot selection) from the parts that don't, whether your code runs, and whether you notice that two gates hitting park() at the same moment is a race.
This lesson runs the round the way an interviewer would, end to end: the clarifying questions, the classes, the two interfaces, the one lock that matters, about 150 lines of Python you can run, and the follow-ups they ask in the last fifteen minutes.
A 90-minute machine-coding round: clarify what fits where, which spot to give and how to charge, then model ParkingLot as the only stateful class with two injected strategies (spot assignment and pricing) over plain data. A car enters, the strategy picks the lowest floor and smallest fitting spot under one lock, a ticket is issued, and at exit 2 h 5 min is billed as 3 hours, ₹120. Without the lock two gates can park in one spot; the demo races 40 cars for 6 spots.
How the round runs#
"Design a parking lot. Multiple floors, different vehicle sizes, tickets at entry, payment at exit. You have 90 minutes. I want working code at the end, with a small demo in main. Ask me anything."
That "ask me anything" is graded. A machine-coding round is not a system design round: nobody cares about load balancers here. It is scored on four things, roughly equally:
- Requirements: did you pin the scope down in 5–10 minutes, or start coding a guess?
- Modelling: are the classes the nouns of the problem, with one job each? Are the things that vary behind interfaces?
- Working code: does it run, does the demo prove the rules, is it readable without you narrating it?
- Extensibility: when they add a requirement at minute 75, how many existing lines do you edit?
- 0–10 min — clarify: vehicle types, spot sizes, pricing, how many gates, what "full" means
- 10–25 min — entities and interfaces on paper (or as empty classes), agreed with the interviewer
- 25–70 min — code it, bottom-up: enums and data classes, the two strategies, then the lot
- 70–80 min — a
mainthat parks, rejects, charges and races two gates - 80–90 min — follow-ups: a new vehicle type, an EV spot, a display board, per-floor locking
Requirements and clarifying questions#
Ask questions whose answers change the code. "How many floors?" does not (it's a number); "can a bike park in a car spot?" does.
| You ask | They answer | What it changes |
|---|---|---|
| Which vehicle types? | Bike, car, truck. | A VehicleType enum |
| Which spot sizes, and what fits where? | Small, medium, large. A vehicle can take its own size or larger. | A fits() rule, not a lookup per pair |
| Which spot do we give? | Nearest to the entrance: lowest floor first. | A SpotAssignmentStrategy |
| How is the fee computed? | Hourly by vehicle type, every started hour billed, minimum one hour. | A PricingStrategy |
| How many entry and exit gates? | Several, working at the same time. | A lock: concurrency is in scope |
| Payment modes, lost tickets, reservations? | Out of scope; mention them at the end. | Nothing now; a follow-up later |
In scope
- Floors with small, medium and large spots
- Park a vehicle: assign a spot, issue a ticket
- Leave: free the spot, compute the fee
- Show free spots per floor and size
- Several gates calling at once
Out of scope (for now)
- Payment processing (cash, UPI, FASTag)
- Reservations and monthly passes
- Lost tickets and penalties
- Persistence; everything lives in memory
Wait for the interviewer to agree before you draw a single class. If they add "EV charging spots" now, you'll design for it; if they add it at minute 80, you'll want a design that absorbs it. Both are fine, as long as you asked.
Before the classes: which parts will change?
Look at the requirements. Which two behaviours are most likely to change after launch, and so belong behind an interface?
One strong answer
Which spot to give (nearest first today; tomorrow "keep the ground floor for EVs" or "fill one floor at a time so the others can be closed at night") and what to charge (hourly today; tomorrow weekend rates, a flat night fee, or a free first 30 minutes for a mall). Both become strategies the lot is given in its constructor. Everything else (a spot, a ticket, a vehicle) is plain data.
The classes#
Pull the nouns out of the problem: lot, floor, spot, vehicle, ticket, receipt. Pull the verbs out too: park, leave, price, find a spot. Nouns become classes; verbs that vary become interfaces.
%% title: Parking lot: the classes
classDiagram
class ParkingLot {
+park(vehicle, at) Ticket
+leave(ticket_id, at) Receipt
+availability() dict
}
class Floor {
+level int
+free_count(size) int
}
class ParkingSpot {
+spot_id str
+size SpotSize
+vehicle Vehicle
+fits(vehicle) bool
}
class Vehicle {
+plate str
+type VehicleType
}
class Ticket {
+ticket_id str
+spot_id str
+entry_at datetime
}
class SpotAssignmentStrategy {
<<interface>>
+find_spot(floors, vehicle) ParkingSpot
}
class PricingStrategy {
<<interface>>
+price(ticket, exit_at) Receipt
}
ParkingLot "1" *-- "many" Floor
Floor "1" *-- "many" ParkingSpot
ParkingSpot --> Vehicle
Ticket --> Vehicle
ParkingLot --> SpotAssignmentStrategy
ParkingLot --> PricingStrategy
SpotAssignmentStrategy <|.. NearestSmallestFit
PricingStrategy <|.. HourlyPricingThree decisions are worth saying out loud while you draw this:
ParkingLotis the only class that changes state. Spots, floors and tickets are data. That gives you exactly one place to put a lock, and one class to test.- Vehicles don't get subclasses.
Car(Vehicle)andBike(Vehicle)with no behaviour of their own are a classic over-design. Atypeenum is enough until a vehicle actually behaves differently. - The ticket stores the spot id, not the spot. A ticket is a record of what happened; it should not keep a live object alive or let someone mutate a spot through it.
The interfaces#
Start with the vocabulary. Frozen dataclasses for things that never change once created (a vehicle, a ticket, a receipt); a mutable one for the spot, whose vehicle changes. Money is an int in rupees; never a float.
# parking_lot.py, part 1: the vocabulary (enums and plain data)
from __future__ import annotations
import itertools
import math
import threading
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from datetime import datetime, timedelta
from enum import Enum
class VehicleType(Enum):
BIKE = "bike"
CAR = "car"
TRUCK = "truck"
class SpotSize(Enum):
SMALL = 1
MEDIUM = 2
LARGE = 3
# the smallest spot each vehicle fits in; anything larger also fits
MIN_SIZE = {VehicleType.BIKE: SpotSize.SMALL, VehicleType.CAR: SpotSize.MEDIUM, VehicleType.TRUCK: SpotSize.LARGE}
@dataclass(frozen=True)
class Vehicle:
plate: str
type: VehicleType
@dataclass
class ParkingSpot:
spot_id: str
floor: int
size: SpotSize
vehicle: Vehicle | None = None
def is_free(self) -> bool:
return self.vehicle is None
def fits(self, v: Vehicle) -> bool:
return self.size.value >= MIN_SIZE[v.type].value
@dataclass
class Floor:
level: int
spots: list[ParkingSpot] = field(default_factory=list)
def free_count(self, size: SpotSize) -> int:
return sum(1 for s in self.spots if s.size is size and s.is_free())
@dataclass(frozen=True)
class Ticket:
ticket_id: str
vehicle: Vehicle
spot_id: str
entry_at: datetime
@dataclass(frozen=True)
class Receipt:
ticket_id: str
hours: int
amount: int # rupeesNow the two seams. Each is one method, so each implementation is small and easy to test on its own. NearestSmallestFit gives the lowest floor first and, on that floor, the smallest spot that fits, so a bike doesn't burn the only large spot a truck could use.
# part 2: the two interfaces, and one implementation of each
class SpotAssignmentStrategy(ABC):
@abstractmethod
def find_spot(self, floors: list[Floor], vehicle: Vehicle) -> ParkingSpot | None: ...
class NearestSmallestFit(SpotAssignmentStrategy):
"""Lowest floor first; on a floor, the smallest spot that fits (keep large spots for trucks)."""
def find_spot(self, floors: list[Floor], vehicle: Vehicle) -> ParkingSpot | None:
for floor in sorted(floors, key=lambda f: f.level):
fitting = [s for s in floor.spots if s.is_free() and s.fits(vehicle)]
if fitting:
return min(fitting, key=lambda s: (s.size.value, s.spot_id))
return None
class PricingStrategy(ABC):
@abstractmethod
def price(self, ticket: Ticket, exit_at: datetime) -> Receipt: ...
class HourlyPricing(PricingStrategy):
"""Every started hour is billed; the minimum is one hour."""
def __init__(self, rates: dict[VehicleType, int]) -> None:
self.rates = rates
def price(self, ticket: Ticket, exit_at: datetime) -> Receipt:
seconds = max(0.0, (exit_at - ticket.entry_at).total_seconds())
hours = max(1, math.ceil(seconds / 3600))
return Receipt(ticket.ticket_id, hours, hours * self.rates[ticket.vehicle.type])The patterns, and why#
| Pattern | Where | Why it earns its place |
|---|---|---|
| Strategy | SpotAssignmentStrategy, PricingStrategy |
The two behaviours the business will change. A weekend tariff is a new class passed to the constructor; ParkingLot is not edited (open/closed). |
| Dependency injection | ParkingLot(floors, assigner, pricing) |
Tests pass a fixed strategy and a fixed time. Nothing reaches for a global. |
| Factory function | build_lot() |
Building floors and spots is noise; keep it out of the class that runs the lot. |
| Singleton (avoided) | — | Many candidates make ParkingLot a singleton. It buys nothing and makes tests share state. Say you'd create one instance at startup and pass it around. |
Concurrency and edge cases#
Two entry gates run on two threads. Both call park(). Both call find_spot(), both see F0-M0 free, both write their car into it. Two tickets, one spot. The fix is to make find and occupy one atomic step: one lock around both. Pricing touches no shared state, so it runs outside the lock.
- Lot full for this vehicle type: raise a specific
LotFullError, not aNonethe caller forgets to check - Same plate twice: a car that is already inside can't park again (a cloned number plate, or a gate glitch)
- Ticket used twice or unknown: reject; the second exit attempt must not free someone else's spot
- Exit time before entry time (clock skew between gate machines): clamp to zero, still bill the minimum hour
- A bike in the only free car spot: allowed by the rules, but the strategy prefers the smallest fit first
- Time is a parameter (
at), neverdatetime.now()inside the class: the demo and the tests control the clock
One lock for the whole lot is correct and simple. If the interviewer pushes on throughput (a 5,000-spot mall, 20 gates), the answer is a lock per floor, and a gate tries floors in order; but a parking lot sees a few cars a second, so start simple and say why.
The code#
The lot itself. Note what is inside the lock and what isn't:
# part 3: the lot itself, the only class that changes state
class LotFullError(Exception): ...
class ParkingLot:
def __init__(self, floors: list[Floor], assigner: SpotAssignmentStrategy, pricing: PricingStrategy) -> None:
self.floors = floors
self.assigner = assigner
self.pricing = pricing
self._spots = {s.spot_id: s for f in floors for s in f.spots}
self._active: dict[str, Ticket] = {} # ticket_id -> ticket
self._parked: set[str] = set() # plates inside right now
self._ids = itertools.count(1)
self._lock = threading.Lock() # entry and exit gates call in on different threads
def park(self, vehicle: Vehicle, at: datetime) -> Ticket:
with self._lock: # find + occupy must be one atomic step
if vehicle.plate in self._parked:
raise ValueError(f"{vehicle.plate} is already inside")
spot = self.assigner.find_spot(self.floors, vehicle)
if spot is None:
raise LotFullError(f"no spot for a {vehicle.type.value}")
spot.vehicle = vehicle
ticket = Ticket(f"T{next(self._ids):04d}", vehicle, spot.spot_id, at)
self._active[ticket.ticket_id] = ticket
self._parked.add(vehicle.plate)
return ticket
def leave(self, ticket_id: str, at: datetime) -> Receipt:
with self._lock:
ticket = self._active.pop(ticket_id, None)
if ticket is None:
raise KeyError(f"unknown or already used ticket {ticket_id}")
self._spots[ticket.spot_id].vehicle = None
self._parked.discard(ticket.vehicle.plate)
return self.pricing.price(ticket, at) # pure, so it can run outside the lock
def availability(self) -> dict[int, dict[str, int]]:
with self._lock:
return {f.level: {z.name: f.free_count(z) for z in SpotSize} for f in self.floors}And the demo: park three vehicles, check availability, charge a 2-hour-5-minute stay, reject a reused ticket, then race 40 cars through the gates at once.
# part 4: build a small lot and drive it
def build_lot(levels: int, layout: dict[SpotSize, int]) -> ParkingLot:
floors = []
for lv in range(levels):
spots = [ParkingSpot(f"F{lv}-{size.name[0]}{i}", lv, size) for size, n in layout.items() for i in range(n)]
floors.append(Floor(lv, spots))
rates = {VehicleType.BIKE: 20, VehicleType.CAR: 40, VehicleType.TRUCK: 100}
return ParkingLot(floors, NearestSmallestFit(), HourlyPricing(rates))
if __name__ == "__main__":
t0 = datetime(2026, 10, 25, 9, 0)
lot = build_lot(2, {SpotSize.SMALL: 2, SpotSize.MEDIUM: 2, SpotSize.LARGE: 1})
car = lot.park(Vehicle("KA01AB1234", VehicleType.CAR), t0)
bike = lot.park(Vehicle("KA05XY9", VehicleType.BIKE), t0)
truck = lot.park(Vehicle("MH12TR77", VehicleType.TRUCK), t0)
print(car.spot_id, bike.spot_id, truck.spot_id) # F0-M0 F0-S0 F0-L0
print(lot.availability()[0])
print(lot.leave(car.ticket_id, t0 + timedelta(hours=2, minutes=5))) # 3 started hours: 120
try:
lot.leave(car.ticket_id, t0 + timedelta(hours=3))
except KeyError as e:
print("rejected:", e)
# 40 cars race through the gates for 6 car-sized spots (2 floors x 2 medium + 1 large)
lot2 = build_lot(2, {SpotSize.SMALL: 2, SpotSize.MEDIUM: 2, SpotSize.LARGE: 1})
won: list[Ticket] = []
def arrive(i: int) -> None:
try:
won.append(lot2.park(Vehicle(f"DL{i:04d}", VehicleType.CAR), t0))
except LotFullError:
pass
threads = [threading.Thread(target=arrive, args=(i,)) for i in range(40)]
for th in threads:
th.start()
for th in threads:
th.join()
spots = [t.spot_id for t in won]
print(len(won), "parked,", len(set(spots)), "distinct spots") # 6 parked, 6 distinct spotsRun it with python3 parking_lot.py:
F0-M0 F0-S0 F0-L0
{'SMALL': 1, 'MEDIUM': 1, 'LARGE': 0}
Receipt(ticket_id='T0001', hours=3, amount=120)
rejected: 'unknown or already used ticket T0001'
6 parked, 6 distinct spotsThe last line is the one that proves the lock: 40 threads, 6 car-sized spots, 6 tickets, no spot given twice. Remove the with self._lock: in park() and run it a few times; sooner or later two tickets share a spot.
Extending it: the follow-ups#
The last 15 minutes are follow-ups. Each one should land as a small, local change; if one forces you to rewrite ParkingLot, the design was wrong.
- "Add an electric-vehicle spot that only EVs may use." Add
SpotSize.EV(or afeaturesset on the spot), add the rule tofits(), and write anEVFirstStrategythat tries charging spots for EVs. The lot is not touched. - "Weekend pricing, and the first 30 minutes free." A new
PricingStrategy, or a small decorator that wraps another pricing (FreeFirst30(HourlyPricing(...))). Pricing classes compose. - "A display board at each entrance showing free spots per floor." Observer: the lot publishes
spot_takenandspot_freedevents, and each board subscribes. Don't make the board pollavailability(). - "Payment by UPI, card or cash." A
PaymentMethodinterface thatleave()hands the receipt to. Payment failures must not free the spot until paid; that is a state on the ticket (ACTIVE → PAID → EXITED). - "Make allocation O(1) for a 10,000-spot lot." Keep a min-heap of free spots per size, keyed by
(floor, spot_id).find_spotbecomes a heap pop instead of a scan; push the spot back onleave(). - "Lost ticket." Look the vehicle up by plate (keep a plate → ticket map, which you already need for the duplicate check) and charge a lost-ticket fee: another pricing rule.
Common mistakes#
- Coding before agreeing the scope. Twenty minutes into a design the interviewer didn't ask for.
- A
ParkingLotgod class with the pricingif/elifchain, the spot search and the printing all insidepark(). - A subclass per vehicle with no behaviour, and then a
isinstanceladder to decide where each one fits. - No lock, or a lock around
find_spotonly, released before the spot is marked taken: the race is still there. - Floats for money and
datetime.now()buried in the class, so the demo can't show a 3-hour bill. - No
main. Code that was never run fails on a typo in front of the interviewer. Run it every 15 minutes.
Parking lot
Q1/4 Two entry gates call park() at the same instant and both get spot F0-M0. What is the minimal correct fix?
Answer & explanation
Answer: B
The race is between "I saw it free" and "I marked it taken". Locking only the search still lets both gates find the same spot before either writes. One lock around both closes the gap; per-gate floors only hide it.
Q2/4 The mall wants the first 30 minutes free on weekends. In this design, what changes?
Answer & explanation
Answer: C
Pricing is a strategy precisely so that a tariff change is a new class. ParkingLot is not edited (open/closed principle).
Q3/4 Why is Vehicle a dataclass with a type enum instead of Car, Bike and Truck subclasses?
Answer & explanation
Answer: B
Subclasses earn their place when they behave differently. Here only "what fits where" differs, and that is one rule on the spot. Empty subclasses add files, not design.
Q4/4 Why does park() take the time as a parameter instead of calling datetime.now()?
Answer & explanation
Answer: D
Hidden clocks make time-based rules untestable. Passing time in (or injecting a Clock) is how the demo shows "2 h 5 min is billed as 3 hours" without waiting three hours.
New lessons by email
New lessons, side quests and case files as they ship, at most once a week. No spam, unsubscribe anytime.
Discussion · …
Public: everyone reading this lesson sees comments. Be kind and keep it on the material.