Parking Lot: the Most-Asked LLD Question
A multi-floor lot with several spot sizes, entry/exit gates, and tickets, where the two decisions that must stay pluggable are how a spot gets picked and how the exit fee gets computed, safely, even when two cars arrive at once.
6.1Problem Statement
Design a multi-floor parking lot system that issues tickets, allocates spots, and charges on exit. It is, by a wide margin, the single most-asked LLD question in the industry — precisely because it looks trivial in the first two minutes and then the interviewer keeps adding requirements to see whether your class boundaries survive the pressure.
Functional Requirements
- The lot has multiple floors, each with spots of several types: Motorcycle, Compact (car), Large (truck/bus).
- On entry, a vehicle is allocated a compatible spot; the system issues a ticket (entry time, spot, vehicle).
- On exit, the ticket is closed, the fee is computed (time-based, possibly per vehicle type), payment is taken, and the spot is freed.
- Entry is rejected when no compatible spot is available (the lot is full for that vehicle type).
- The lot supports multiple entry/exit gates operating at the same time.
Non-Functional Requirements
- Spot-allocation strategy must be pluggable: nearest-to-entrance, first-available, per-floor load balancing.
- Pricing strategy must be pluggable: flat hourly, per-vehicle-type, day/night rates, free first 15 minutes.
- Thread safety: concurrent entries at different gates must never be handed the same spot.
The interface an interviewer often sketches up front looks like this — notice that the two strategies are named in the contract before a single line of them exists:
enum class VehicleType { MOTORCYCLE, CAR, TRUCK };
class SpotAllocationStrategy { // Strategy
public:
virtual ParkingSpot* findSpot(VehicleType, const ParkingLot&) = 0;
virtual ~SpotAllocationStrategy() = default;
};
class PricingStrategy { // Strategy
public:
virtual double calculate(const Ticket&) const = 0;
virtual ~PricingStrategy() = default;
};
class ParkingLot {
public:
Ticket parkVehicle(const Vehicle& v); // throws if full
Receipt unparkVehicle(const Ticket& t); // computes fee, frees spot
bool isFull(VehicleType) const;
};
What the Interviewer Is Really Testing
- Clean modeling of
ParkingLot → Floor → ParkingSpot, theVehiclehierarchy, andTicket. - Whether allocation and pricing are actually pluggable — they will swap the rule mid-interview and watch what you have to touch to satisfy it.
- Your answer for the concurrency question: a lock per floor, an atomic claim, or something worse that lets two gates double-book a bay.
Follow-Up Questions to Expect
- EV spots with charging, handicapped-reserved spots.
- Reservation / pre-booking ahead of arrival.
- Find-my-car and real-time per-floor availability display.
- Lost-ticket handling and a flat penalty fee.
6.2How to Approach
Eight minutes of clarifying questions before any class gets written buys you the rest of the hour. Lock down: one lot, multiple floors; three vehicle types against three spot sizes with a fit rule (a bike fits any bay, a car needs Medium or larger, a truck needs Large); hourly pricing by vehicle type, minimum one hour, rounded up; nearest-first assignment (the first free compatible bay, floor by floor, in order). Say that scope back to the interviewer before touching the keyboard — it becomes the contract for the rest of the hour.
The entity model falls out of that scope almost mechanically:
ParkingLot the front door; the only thing the outside world calls
├── has many ParkingFloor one level
│ └── has many ParkingSpot one bay (owns its size + who's in it)
│ └── points to a Vehicle while occupied
├── keeps a list of Ticket one per parked vehicle
└── fee logic an internal detail, not a caller's business
Vehicle ── is the base of ──▸ Motorcycle | Car | Truck
Two modeling habits are worth stating out loud in the interview, because they are what “properly encapsulated” actually means here, not just a compliment word:
- Put each rule on the object that owns the data it needs.
canFitlives onParkingSpotbecause only the spot knows its own size; a floor never reaches into a spot's internals to decide, it just asks the spot. - Nobody flips state directly. A spot's occupied flag only changes inside
park()/leave().ParkingLotis the only front door the outside world calls — nothing outside ever reaches into aParkingFloororParkingSpotto poke at its fields.
ParkingLot as a hotel concierge, not a hotel. The concierge does not personally check every room; they ask the floor, the floor asks the room. The moment you catch yourself writing a loop inside ParkingLot that reaches past ParkingFloor into a spot's internals, you have broken the chain of command the whole design depends on.The extensibility insight interviewers are fishing for: two things in this design change constantly across follow-ups — how a spot is picked and how a fee is computed — and neither should require editing ParkingLot itself. That is exactly what SpotAllocationStrategy and PricingStrategy are for: two narrow interfaces, one method each, handed to the lot at construction time by a small factory that knows which concrete policy a given deployment wants.
class SpotAllocationStrategy {
public:
virtual ParkingSpot* findSpot(VehicleType type, ParkingLot& lot) = 0;
virtual ~SpotAllocationStrategy() = default;
};
class NearestFirstAllocation : public SpotAllocationStrategy { /* today's linear scan, promoted */ };
class LoadBalancedAllocation : public SpotAllocationStrategy { /* picks the least-full floor */ };
class PricingStrategy {
public:
virtual double price(const Ticket& t, double hours) const = 0;
virtual ~PricingStrategy() = default;
};
class HourlyByVehicleType : public PricingStrategy { /* today's calculateFee, promoted */ };
class DayNightSurge : public PricingStrategy { /* new rule, new class, ParkingLot untouched */ };
class StrategyFactory {
public:
static unique_ptr<SpotAllocationStrategy> makeAllocation(AllocationPolicy p);
static unique_ptr<PricingStrategy> makePricing(PricingPolicy p);
};
- Strategy pattern
- Pull an algorithm that varies (how to pick a spot, how to price a stay) behind a one-method interface, and hand the object that needs it a pointer to whichever implementation applies. New policy = new class; the caller's code never changes.
- Factory
- A small piece of code whose only job is constructing the right concrete strategy for a given configuration, so the wiring decision lives in one place instead of being scattered through the codebase.
- Race condition
- Two threads read the same “is this spot free?” answer before either has written its claim, so both proceed to occupy the same bay. The bug does not show up in a single-threaded demo; it shows up in production at the two busiest gates.
- Critical section
- The sequence “find a free compatible spot, then mark it occupied” has to happen as one indivisible unit. Anything less — checking free-ness and claiming it as two separate unguarded steps — is where the race lives.
Be honest about the gap between this design and the file in the next section, because an interviewer who reads code will notice it before you finish talking. parking-lot.cpp does not implement SpotAllocationStrategy or PricingStrategy. ParkingFloor::findFreeSpot is a hardcoded linear scan calling ParkingSpot::canFit; ParkingLot::calculateFee is a hardcoded set of if conditionals living directly inside ParkingLot. There is no mutex anywhere and no atomic claim — two gates calling parkVehicle at the same instant against the same floor can both walk the vector, both see the same free spot, and both call park() on it.
Say this straight in the interview rather than let the code speak for itself: “here is the working OOP skeleton — the Vehicle hierarchy, ParkingSpot/Floor/Ticket/Lot, everything correctly encapsulated. Today findFreeSpot and calculateFee are hardcoded; here is exactly how I'd lift each behind its own interface, and here is where a std::mutex — one per floor for less contention, or one guarding the whole parkVehicle/unparkVehicle call for simplicity — has to go before two gates can safely run concurrently.” Naming the gap and the fix out loud is worth more than code that quietly pretends the gap does not exist.
6.3Whole Solution
The complete, compiling file below is the skeleton described above: enums, the Vehicle hierarchy, ParkingSpot with its canFit rule, ParkingFloor, Ticket, and ParkingLot as the single front door, plus a main that parks three vehicles, exits one, and reuses its freed bay. As flagged above, allocation and pricing are hardcoded here rather than pulled behind the Strategy interfaces, and there is no locking — both are the honest next step, not a hidden defect.
#include <bits/stdc++.h>
using namespace std;
enum class VehicleType{
MOTORCYCLE,
CAR,
TRUCK
};
enum class SpotType{
SMALL,
MEDIUM,
LARGE
};
string toString(VehicleType type) {
if (type == VehicleType::MOTORCYCLE) return "Motorcycle";
if (type == VehicleType::CAR) return "Car";
if (type == VehicleType::TRUCK) return "Truck";
return "Unknown";
}
class Vehicle{
private:
string licencePlate_;
VehicleType type_;
public:
Vehicle(string plate, VehicleType type){
licencePlate_ = plate;
type_ = type;
}
string getLicencePlate() const {
return licencePlate_;
}
VehicleType getVehicleType() const {
return type_;
}
};
class Motorcycle : public Vehicle {
public:
Motorcycle(string plate) : Vehicle(plate, VehicleType::MOTORCYCLE) {}
};
class Car : public Vehicle {
public:
Car(string plate) : Vehicle(plate, VehicleType::CAR) {}
};
class Truck : public Vehicle {
public:
Truck(string plate) : Vehicle(plate, VehicleType::TRUCK) {}
};
class ParkingSpot{
private:
string id_;
SpotType type_;
bool occupied_;
Vehicle* parkedVehicle_;
public:
ParkingSpot(string id, SpotType type){
id_ = id;
type_ = type;
occupied_ = false;
parkedVehicle_ = nullptr;
}
string getId() const { return id_; }
bool isFree() const { return !occupied_; }
bool canFit(VehicleType vehicle) const {
if(vehicle == VehicleType::MOTORCYCLE){ return true; }
if(vehicle == VehicleType::CAR){
return type_ == SpotType::MEDIUM || type_ == SpotType::LARGE;
}
if(vehicle == VehicleType::TRUCK){
return type_ == SpotType::LARGE;
}
return false;
}
void park(Vehicle* vehicle){
parkedVehicle_ = vehicle;
occupied_ = true;
}
void leave(){
parkedVehicle_ = nullptr;
occupied_ = false;
}
};
class ParkingFloor{
private:
int number_;
vector<ParkingSpot> spots_;
public:
ParkingFloor(int number){
number_ = number;
}
int getNumber(){
return number_;
}
void addSpot(string id, SpotType type){
spots_.push_back(ParkingSpot(id, type));
}
ParkingSpot* findFreeSpot(VehicleType vt){
for(ParkingSpot& spot : spots_){
if(spot.isFree() && spot.canFit(vt)){
return &spot;
}
}
return nullptr;
}
ParkingSpot* findSpotById(string id){
for(ParkingSpot& spot : spots_){
if(spot.getId() == id){
return &spot;
}
}
return nullptr;
}
int countFree(){
int count = 0;
for(ParkingSpot& spot : spots_){
if(spot.isFree()){
count++;
}
}
return count;
}
};
class Ticket{
private:
string id_;
string spotId_;
string plate_;
VehicleType vehicleType_;
public:
Ticket(string id, string spotId, string plate, VehicleType type){
id_ = id;
spotId_ = spotId;
plate_ = plate;
vehicleType_ = type;
}
string getId(){
return id_;
}
string getSpotId(){
return spotId_;
}
VehicleType getVehicleType(){
return vehicleType_;
}
};
class ParkingLot{
private:
vector<ParkingFloor> floors_;
vector<Ticket> tickets_;
int ticketCounter_ = 0;
double calculateFee(VehicleType type, double hours){
double billableHours = ceil(hours);
if(billableHours < 1) billableHours = 1;
double rate = 0;
if(type == VehicleType::MOTORCYCLE) rate = 10;
if(type == VehicleType::CAR) rate = 20;
if(type == VehicleType::TRUCK) rate = 40;
return rate * billableHours;
}
public:
void addFloor(int number){
floors_.push_back(ParkingFloor(number));
}
void addSpot(string id, SpotType type){
floors_.back().addSpot(id, type);
}
void parkVehicle(Vehicle* vehicle){
for(ParkingFloor& floor : floors_){
ParkingSpot* spot = floor.findFreeSpot(vehicle->getVehicleType());
if(spot != nullptr){
spot->park(vehicle);
ticketCounter_++;
string ticketId = "T" + to_string(ticketCounter_);
tickets_.push_back(Ticket(ticketId, spot->getId(), vehicle->getLicencePlate(), vehicle->getVehicleType()));
cout << "[Park] " << toString(vehicle->getVehicleType()) << " " << vehicle->getLicencePlate()
<< " -> spot " << spot->getId() << " (ticket " << ticketId << ")" << endl;
return;
}
}
cout << "[FULL] no spot for " << toString(vehicle->getVehicleType())
<< " " << vehicle->getLicencePlate() << "\n";
}
double unparkVehicle(string ticketId, double hours){
for(int i = 0; i < (int)tickets_.size(); i++){
if(tickets_[i].getId() == ticketId){
Ticket ticket = tickets_[i];
for(ParkingFloor& floor : floors_){
ParkingSpot* spot = floor.findSpotById(ticket.getSpotId());
if(spot != nullptr) spot->leave();
}
double fee = calculateFee(ticket.getVehicleType(), hours);
cout << "[EXIT] ticket " << ticketId << " from spot "
<< ticket.getSpotId() << " | " << hours << "h -> fee $" << fee << "\n";
tickets_.erase(tickets_.begin() + i);
return fee;
}
}
cout << "[ERROR] invalid ticket " << ticketId << "\n";
return -1;
}
void printAvailability() {
cout << "--- availability ---\n";
for (ParkingFloor& floor : floors_) {
cout << " floor " << floor.getNumber() << ": "
<< floor.countFree() << " free\n";
}
}
};
int main(){
ParkingLot lot;
lot.addFloor(1);
lot.addSpot("F1-S1", SpotType::SMALL);
lot.addSpot("F1-M1", SpotType::MEDIUM);
lot.addSpot("F1-L1", SpotType::LARGE);
lot.addFloor(2);
lot.addSpot("F2-M1", SpotType::MEDIUM);
lot.addSpot("F2-L1", SpotType::LARGE);
lot.printAvailability();
Motorcycle bike("KA01-1111");
Car car("KA02-2222");
Truck truck("KA03-3333");
lot.parkVehicle(&bike);
lot.parkVehicle(&car);
lot.parkVehicle(&truck);
lot.printAvailability();
lot.unparkVehicle("T2", 2.5); // car: ceil(2.5)=3, rate 20 -> $60
lot.printAvailability();
Car car2("KA04-4444"); // reuses F1-M1 that the first car freed
lot.parkVehicle(&car2);
lot.printAvailability();
return 0;
}
Two cars enter through different gates at the same instant and both find the same free bay. How do you actually fix this?
You have to guard the whole check-then-claim sequence, not just the scan. Reading “is this spot free?” and writing “now it's occupied” must happen as one atomic unit. The simplest fix is a std::mutex per floor around findFreeSpot + park, taken as a single critical section; that gives you concurrency across floors while serializing contention within one. A lock-free alternative is a per-spot atomic flag with compare-and-swap: try to flip occupied_ from false to true, and if it fails, move to the next candidate spot instead of retrying the same one. Either answer is acceptable; naming the race and picking one is the point.
How would you add EV charging spots or handicapped-reserved spots without rewriting ParkingLot?
Extend SpotType with the new values and add an eligibility check next to canFit: not just “does the vehicle fit the size” but “is this vehicle allowed in this spot” (permit on file, EV flag on the vehicle). That check lives on ParkingSpot for the same reason canFit does — it depends on data the spot owns. The allocation strategy calls both checks before offering a bay. Nothing about Vehicle, Ticket, or ParkingLot's public surface has to change.
How do you support find-my-car and a live per-floor availability display without touching the core park/unpark flow?
countFree() already gives per-floor availability for free; a dashboard just polls it, or better, spots publish a state change on park()/leave() to a small event bus the display subscribes to, so nobody polls at all. Find-my-car is a lookup from license plate to ticket to spot id, which today means scanning tickets_ linearly. The fix is the same one sol.md flags for O(1) exit: keep a map<string, ParkingSpot*> and a plate-to-ticket index alongside the vector, so both lookups become constant time instead of a scan.