Design a Parking Lot
OOP design for a multi-level parking lot covering vehicle types, spot allocation strategies, ticket management, payment processing, and capacity tracking with thread-safe operations.
The Problem
Your company manages 12 multi-level parking garages across a metropolitan area. The current system is entirely manual: attendants wave cars into the garage and hand-write tickets. During peak hours, drivers circle for 10 minutes looking for an open spot because nobody knows which floors still have availability. Last month, a truck parked in a compact spot and blocked two adjacent vehicles for six hours.
A well-designed parking lot system solves all of this. It assigns vehicles to compatible spots automatically, tracks capacity per floor in real time, issues tickets with timestamps, and calculates fees at exit. The tricky parts? Different vehicle types need different spot sizes, multiple allocation strategies exist (nearest to entrance, spread across floors, compact-first), and multiple entry/exit panels operate concurrently.
Design the core classes for a multi-level parking lot that handles vehicle-to-spot matching, pluggable spot allocation strategies, ticket-based entry and exit, hourly payment with per-vehicle-type rates, per-floor capacity tracking, and thread-safe concurrent operations.
Requirements
Clarifying Questions
Before jumping into class design, ask questions to turn the vague prompt into a concrete specification. Cover four areas: core actions, error handling, boundaries, and future extensions.
You: "How many floors does the parking lot have, and is it a fixed number?"
Interviewer: "Multiple floors, configurable at construction time. Each floor has a fixed number of spots of various types."
Good. Floors are configured once at startup, not dynamically added. That simplifies the model.
You: "What vehicle types should the system support? Just cars?"
Interviewer: "Four types: motorcycles, cars, trucks, and vans. Each has different size requirements."
Four vehicle types. Each type fits into specific spot types. This means we need a mapping between vehicle types and compatible spot types.
You: "What kinds of parking spots exist, and which vehicles fit in which spots?"
Interviewer: "Four spot types: MOTORCYCLE, COMPACT, LARGE, and HANDICAPPED. Motorcycles go in motorcycle or compact spots. Cars go in compact, large, or handicapped. Trucks only fit in large spots. Vans fit in large or handicapped."
That mapping is the core allocation logic. Motorcycles are the most flexible, trucks the least. I will encode this as a per-vehicle-type compatibility list.
You: "When a vehicle arrives, how is a spot chosen? Nearest to entrance? Random?"
Interviewer: "Support multiple strategies: nearest to entrance (lowest floor first), spread evenly across floors (balance load), and compact-first (fill smaller spots before larger). The strategy should be swappable."
Multiple allocation strategies with runtime selection. Classic Strategy pattern. The lot delegates the "pick a spot" decision to a strategy object.
You: "How does payment work? Flat fee or hourly?"
Interviewer: "Hourly rate, different for each vehicle type. A motorcycle pays less per hour than a truck. The rate calculation should be pluggable so we can add weekend surcharges or flat-rate events later."
Another Strategy pattern for rate calculation. Separating the rate logic from the payment flow keeps things extensible.
You: "What happens if the lot is full for a particular vehicle type?"
Interviewer: "Reject the vehicle at the entry panel. The system should be able to answer 'is there space for a car?' in constant time, not by scanning every spot."
O(1) availability check. That means maintaining counters per spot type per floor, not iterating spots on every entry request.
You: "Are there separate entry and exit points? Can multiple vehicles enter or exit simultaneously?"
Interviewer: "Yes. Multiple entry panels and exit panels. Two cars could arrive at different entry panels at the same instant. Spot assignment must be thread-safe to avoid double-booking."
Concurrent entry and exit. Atomic spot assignment is critical. I will use synchronization at the spot-assignment level.
You: "Should the system support reservations or EV charging spots?"
Interviewer: "Out of scope for now, but design it so those could be added later."
Perfect. You have now clarified scope and ruled out unnecessary complexity.
Final Requirements
Functional Requirements:
- Vehicles enter through an EntryPanel and receive a ParkingTicket with entry time and assigned spot
- Support four vehicle types (Motorcycle, Car, Truck, Van) and four spot types (MOTORCYCLE, COMPACT, LARGE, HANDICAPPED) with a defined compatibility mapping
- Pluggable spot allocation strategy: nearest-to-entrance, spread-across-floors, compact-first
- Vehicles exit through an ExitPanel, pay based on hourly rate per vehicle type, and free the spot
- Per-floor and per-spot-type availability tracking with O(1) capacity checks
- Reject vehicles at entry when no compatible spot is available
Non-Functional Requirements:
- Thread-safe concurrent entry and exit across multiple panels
- Pluggable payment/rate strategy via Strategy pattern so new pricing models require zero changes to existing code
- Extensible for new spot types (EV charging) and features (reservations) without modifying core classes
Out of Scope:
- Reservation system
- EV charging management
- Valet service
- UI / display boards
- Persistence / database
Interview tip: number your requirements
Writing numbered requirements on the whiteboard shows the interviewer you are methodical. It also gives you reference points: "Requirement 3 drives the Strategy pattern choice for allocation."
Example Inputs and Outputs
Scenario 1: Car enters, parks, pays, exits
- Input: A Car arrives at EntryPanel #1
- Expected: System finds a COMPACT spot on Floor 1 (nearest-to-entrance strategy), assigns Spot C-12, issues Ticket #1001 with entry time 09:00
- After 3 hours, car exits at ExitPanel #1, pays 3 x $3.00/hr = $9.00, spot C-12 is freed
- Why: Validates the full entry-to-exit lifecycle (Requirements 1, 4, 5)
Scenario 2: Truck requires a LARGE spot
- Input: A Truck arrives. Floor 1 has no LARGE spots available, Floor 2 has one
- Expected: System skips Floor 1, assigns LARGE spot L-05 on Floor 2, issues ticket
- Why: Validates vehicle-to-spot compatibility and multi-floor search (Requirements 2, 3)
Scenario 3: Lot full for motorcycles
- Input: A Motorcycle arrives. All MOTORCYCLE and COMPACT spots are occupied
- Expected: EntryPanel displays "No spots available for motorcycles," vehicle is rejected
- Why: Validates O(1) capacity check and rejection (Requirements 5, 6)
Try It Yourself
Try it yourself
Before reading the solution, spend 15-20 minutes sketching your own class diagram. Focus on the vehicle-to-spot compatibility mapping and how you would track per-floor availability without scanning every spot. Compare your approach with the walkthrough below.
Step 1: Identify Core Entities
Start by asking: what are the main "things" in this problem? Look for nouns in your requirements: parking lot, floor, spot, vehicle, ticket, payment, entry panel, exit panel.
A common mistake is putting everything into a single ParkingLot god class. Good design means each class has one clear job. Another trap is combining ParkingSpot and Vehicle into one class. They have completely different lifecycles: spots are permanent fixtures, vehicles come and go.
| Entity | Responsibility | Key attributes |
|---|---|---|
| ParkingLot | Top-level orchestrator. Holds floors, delegates allocation to a strategy, issues tickets. | floors, allocationStrategy, activeTickets |
| Floor | Manages spots on a single level. Tracks available counts per spot type. | floorNumber, spots, availableCounts |
| ParkingSpot | A single spot with a type and occupancy state. Knows whether it can fit a given vehicle. | spotId, spotType, floor, currentVehicle |
| Vehicle | Data holder with type and license plate. No parking logic. | licensePlate, vehicleType |
| ParkingTicket | Receipt linking a vehicle to a spot with entry time. Tracks payment status. | ticketId, vehicle, spot, entryTime, paymentStatus |
| Payment | Value object representing a completed payment. | amount, paidAt |
| EntryPanel | Triggers vehicle entry. Asks ParkingLot to assign a spot and issue a ticket. | panelId |
| ExitPanel | Triggers vehicle exit. Calculates fee, processes payment, frees the spot. | panelId |
Notice we separated Floor from ParkingLot because each floor independently tracks its own capacity. ParkingLot orchestrates across floors but never directly manipulates individual spots. This keeps the per-floor logic self-contained and testable.
Step 2: Define Relationships and Class Design
Class Diagram
Deriving the Core Classes
ParkingLot (Orchestrator)
The lot is the central coordinator. It never touches individual spots directly; it delegates to floors and strategies.
Deriving state from requirements:
| Requirement | What ParkingLot must track |
|---|---|
| "Vehicles enter and receive a ticket" | Active tickets mapped by ticket ID |
| "Multiple floors with spots" | List of Floor objects |
| "Pluggable allocation strategy" | The current AllocationStrategy |
| "Reject when full" | Ability to check availability (delegates to floors) |
Deriving methods from needs:
| Need from requirements | Method |
|---|---|
| "Issue ticket at entry" | issueTicket(vehicle): ParkingTicket |
| "Process exit and free spot" | processExit(ticket): Payment |
| "O(1) availability check" | hasSpaceFor(vehicleType): boolean |
Floor (Capacity Tracker)
Each floor owns its spots and maintains counters. The counters give us O(1) availability checks without scanning.
Deriving state from requirements:
| Requirement | What Floor must track |
|---|---|
| "Per-floor capacity tracking" | Map of SpotType to available count |
| "Floor has spots" | List of ParkingSpot objects |
| "Identify which floor" | Floor number |
Deriving methods from needs:
| Need from requirements | Method |
|---|---|
| "Find a compatible spot on this floor" | findAvailableSpot(vehicleType): ParkingSpot |
| "O(1) count check" | getAvailableCount(spotType): int |
| "Update counters on park/free" | decrementCount(spotType) / incrementCount(spotType) |
We keep counters synchronized with actual spot state. When a spot is parked in, the floor decrements the counter for that spot type. When freed, it increments.
Key Relationship Decisions
ParkingLot owns Floors (composition) because floors do not exist without a lot. ParkingSpot holds a Vehicle reference (aggregation) because vehicles exist independently. ParkingTicket references both ParkingSpot and Vehicle but does not own either.
The two Strategies (AllocationStrategy and RateStrategy) are injected from the outside, keeping ParkingLot and ExitPanel oblivious to the concrete algorithms.
Step 3: Choose Design Patterns
This is where your design stops being "a bunch of classes" and becomes a principled architecture. Every pattern choice here comes from a signal in the requirements.
Pattern 1: Strategy for Spot Allocation
The signal: "Support multiple allocation strategies: nearest-to-entrance, spread-across-floors, compact-first. The strategy should be swappable." Multiple interchangeable algorithms behind a common interface is the textbook Strategy trigger.
Why Strategy over alternatives: We could use if-else chains inside ParkingLot.findSpot(), but that violates OCP. Every new allocation approach means editing the lot class. With Strategy, adding a new algorithm is a single new class.
How it maps to our entities:
- Interface =
AllocationStrategy - Concrete implementations =
NearestFirstStrategy,SpreadEvenlyStrategy,CompactFirstStrategy - Context =
ParkingLotholds a reference and delegates
Pattern 2: Strategy for Rate Calculation
The signal: "Hourly rate, different per vehicle type. The rate calculation should be pluggable." Same signal, same pattern.
How it maps:
- Interface =
RateStrategy - Concrete implementations =
HourlyRateStrategy,WeekendSurchargeStrategy,FlatRateStrategy - Context =
ExitPaneldelegates fee calculation to the strategy
In an interview, you would say: "I see two independent dimensions of variation, allocation and pricing, so I use two separate Strategy hierarchies. They evolve independently."
For your interview: mention both strategies in one sentence, then spend your time on the allocation strategy because that is the more interesting design challenge.
Vehicle-to-Spot Compatibility: Enum over Inheritance
I chose to encode the vehicle-to-spot mapping in the VehicleType enum rather than creating an abstract Vehicle class hierarchy. Why? Because the compatibility rules are static data, not behavior. A Car does not "do" anything differently from a Truck in this system; it just fits different spots. An enum with a getCompatibleSpotTypes() method is simpler and more maintainable than four subclasses that only differ in a list of spot types.
Step 4: Build the Solution
Pseudocode for Key Methods
Before writing Java, let me walk through the two most interesting methods: issuing a ticket on entry and processing an exit.
issueTicket: The Entry Flow
Core logic (happy path):
- Check if any compatible spot exists (O(1) counter check)
- Delegate to AllocationStrategy to pick a specific spot
- Mark the spot as occupied, decrement floor counter
- Create a ParkingTicket with entry time and spot reference
- Store ticket in active tickets map, return it
Edge cases (reject before touching state):
- No compatible spot available: return null (or Optional.empty)
- Vehicle already parked (duplicate license plate): reject
function issueTicket(vehicle):
if activeTickets.containsKey(vehicle.licensePlate):
return null // already parked
spot = allocationStrategy.findSpot(floors, vehicle.type)
if spot == null:
return null // lot full for this type
spot.park(vehicle) // marks occupied
spot.getFloor().decrementCount(spot.type)
ticket = new ParkingTicket(nextId(), vehicle, spot, now())
activeTickets.put(vehicle.licensePlate, ticket)
return ticket
The entire method must be synchronized or use a lock to prevent two threads from assigning the same spot. I keep the critical section small: find-spot + park + decrement is atomic.
processExit: The Payment Flow
Core logic (happy path):
- Look up ticket, calculate duration
- Delegate fee calculation to RateStrategy
- Mark ticket as paid
- Free the spot, increment floor counter
- Remove from active tickets
Edge cases:
- Ticket not found or already paid: reject
- Duration less than minimum (free period): charge nothing
function processExit(ticket):
if ticket.status == PAID:
return error("already processed")
exitTime = now()
fee = rateStrategy.calculateFee(ticket, exitTime)
payment = new Payment(fee, exitTime)
ticket.markPaid(payment)
spot = ticket.getSpot()
spot.free()
spot.getFloor().incrementCount(spot.type)
activeTickets.remove(ticket.vehicle.licensePlate)
return payment
Full Implementation
// Enum for the four physical spot types in the garage.
// Each type has a display name for ticket printing.
public enum SpotType {
MOTORCYCLE, COMPACT, LARGE, HANDICAPPED;
}
Architecture note: The code follows three SOLID principles. SRP: each class has one job (Floor tracks capacity, ParkingSpot manages occupancy, ParkingLot orchestrates). OCP: new allocation or pricing strategies require zero edits to existing classes. DIP: ParkingLot depends on the AllocationStrategy interface, not on any concrete strategy.
Step 5: Trace a Scenario
Let us walk through Scenario 1 from the Examples section: a Car arrives, parks for 3 hours, pays, and exits. The allocation strategy is NearestFirstStrategy.
Here is the step-by-step breakdown:
Driverarrives atEntryPanel #1and callsscanVehicle(car)where car isnew Vehicle("ABC-123", VehicleType.CAR)EntryPaneldelegates toParkingLot.issueTicket(car), which acquires the synchronized lockParkingLotchecksactiveTicketsfor "ABC-123" (not found, so not a duplicate)ParkingLotcallsstrategy.findSpot(floors, CAR). NearestFirstStrategy iterates compatible types: COMPACT first- Strategy calls
floor1.pollFreeSpot(COMPACT), which removes Spot C-12 from the free set and returns it ParkingLotcallsspot.park(car), setting the spot's currentVehicle reference- A
ParkingTicketis created with ID "T-1001", entry time 09:00, referencing Spot C-12 and the car - Ticket stored in
activeTickets, lock released, ticket returned to driver - Three hours later, driver presents ticket at
ExitPanel #1 ExitPanelcallsParkingLot.processExit(ticket), lock acquiredHourlyRateStrategy.calculateFeeCents()computes: 3 hours x 300 cents/hr (CAR rate) = 900 centsSpot.free()clears the vehicle reference,Floor.returnSpot()adds C-12 back to the COMPACT free set- Ticket marked PAID, removed from active tickets, Payment returned to driver
Extensibility
If time allows, interviewers add follow-up twists. Here is how the design handles the most common ones.
1. "How would you add EV charging spots?"
"Today I have four SpotTypes in an enum. For EV charging, I would add EV_CHARGING to SpotType and create a new entry in VehicleType compatibility lists for electric vehicles. The AllocationStrategy interface does not change. Only VehicleType and SpotType get new enum values. If the interviewer wants charging session tracking, I would add a ChargingSession class that wraps a ParkingSpot with charge level and rate, similar to how ParkingTicket wraps a spot with time and payment."
This works because OCP: the strategy and lot classes never mention specific spot types by name.
2. "How would you add a reservation system?"
"I would add a Reservation class that pre-assigns a spot for a future time window. The Floor.pollFreeSpot() method would check a reservations set and skip spots reserved for the upcoming window. The rest of the system is untouched: ParkingLot still delegates to the strategy, the strategy still calls pollFreeSpot(). The reservation logic lives entirely in Floor."
// Sketch: reservation-aware spot polling
public ParkingSpot pollFreeSpot(SpotType type, Instant arrivalTime) {
Set<ParkingSpot> free = freeSpots.get(type);
for (ParkingSpot spot : free) {
if (!isReserved(spot, arrivalTime)) {
free.remove(spot);
return spot;
}
}
return null;
}
3. "How would you add dynamic pricing (surge pricing at peak hours)?"
"My RateStrategy interface already isolates pricing. I would create a SurgePricingStrategy that wraps the base HourlyRateStrategy (Decorator pattern). It checks the current time, applies a multiplier during peak hours, and delegates to the base strategy for the standard calculation. No changes to ExitPanel or ParkingLot."
4. "How would you support a valet service?"
"Valet is a different entry flow: the driver hands off the vehicle, the valet parks it. I would add a ValetPanel that extends the entry workflow. It calls ParkingLot.issueTicket() just like EntryPanel but also records the valet assignment. The core parking logic does not change, only the panel interface expands."
Common Interview Mistakes
| Mistake | Why it is wrong | What to do instead |
|---|---|---|
| Starting with code immediately | You miss requirements and build the wrong abstractions | Spend 5 minutes on entities and requirements first |
| Making ParkingLot a god class | One class doing allocation, payment, display, and gate control | Split responsibilities: lot orchestrates, floors track capacity, panels handle I/O |
| Hardcoding vehicle-to-spot rules in if-else chains | Adding a new vehicle type requires editing every branch | Use enum compatibility lists or a Strategy |
| Scanning all spots to check availability | O(n) per entry request under load | Maintain per-type counters or free sets for O(1) checks |
| Ignoring thread safety | Two threads assign the same spot to different cars | Synchronize the spot-assignment critical section |
| Using Singleton for ParkingLot | Not everything needs global access, and it makes testing harder | Inject the lot into panels via constructor |
Interview pro tips:
State your requirements before drawing a single class. The interviewer wants to see a methodical process, not a brain dump of classes.
When you draw the class diagram, label every arrow. "ParkingLot contains Floors" is far more informative than an unlabeled line.
Mention thread safety proactively. Even if the interviewer does not ask, saying "I would synchronize spot assignment to handle concurrent entry panels" signals senior-level thinking.
Test Your Understanding
Interview Cheat Sheet
Use this as a quick reference before your interview or during a timed practice session.
| Aspect | Key point |
|---|---|
| Entities | ParkingLot, Floor, ParkingSpot, Vehicle, ParkingTicket, Payment, EntryPanel, ExitPanel |
| Core pattern | Strategy for allocation (NearestFirst, SpreadEvenly, CompactFirst) and for pricing (Hourly, Surge, Flat) |
| Vehicle-Spot mapping | Enum-based: each VehicleType lists its compatible SpotTypes |
| Capacity tracking | Per-type free sets on each Floor for O(1) availability |
| Thread safety | synchronized on issueTicket/processExit to prevent double-booking |
| Extension points | New VehicleType/SpotType: add enum value. New strategy: add class. New panel: implement entry/exit interface. |
| Common follow-ups | EV charging (new SpotType), reservations (pre-assign spots), dynamic pricing (Decorator on RateStrategy), valet (new panel type) |
Quick Recap
- Start every LLD problem by listing entities and their responsibilities before writing a single line of code.
- Vehicle-to-spot compatibility is static data, not behavior. An enum with a compatibility list beats a class hierarchy when the types do not differ in methods.
- Strategy pattern fits when the requirements say "pluggable" or "multiple algorithms." In this system, both allocation and pricing are Strategy candidates.
- O(1) capacity checks require dedicated counters or free sets, not linear scans. The small overhead of maintaining them pays off on every entry request.
- Thread safety matters the moment you have multiple entry/exit panels. Synchronize the spot-assignment critical section, not individual field access.
- Extensibility is not about predicting the future. It is about keeping classes closed to modification and open to extension. New strategies, spot types, and vehicle types should never require editing existing code.
- In your interview, walk through a concrete scenario end-to-end after presenting your design. It proves the design works and shows the interviewer you can trace code, not just draw boxes.