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. Encode it 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. 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 keeps the process methodical. It also gives useful reference points: "Requirement 3 drives the Strategy pattern choice for allocation."
30-Second Design Summary
ParkingLot orchestrates entry and exit while Floor owns spot collections and O(1) availability counts. Vehicle describes the incoming object, ParkingSpot owns occupancy and compatibility, and ParkingTicket records the active assignment. Allocation and pricing are independent strategies. The critical invariant is that selecting a compatible free spot, marking it occupied, updating counts, and issuing the ticket happen atomically; exit reverses those changes only after payment succeeds according to policy.
5-Minute Walkthrough
- Define the boundary. The core covers four vehicle/spot types, entry tickets, exit payment, allocation strategies, and per-floor capacity. Reservations, EV charging, valet flow, UI, and persistence are extensions.
- Put ownership in the right place.
Floormanages its spots and counts;ParkingLotcoordinates floors and strategies; panels translate physical entry/exit actions into domain calls. - Issue a ticket. Check compatibility and capacity, ask the allocation strategy for a spot, claim it, decrement the floor count, and record the vehicle/ticket relationship as one operation.
- Process exit. Look up the ticket, calculate the fee with the rate strategy, collect payment, mark the spot free, increment the right availability count, and close the ticket. Failed payment must leave occupancy unchanged.
- Extend behind interfaces. New allocation, pricing, payment, reservation, or spot types should add policy/classes at the boundary. The floor-level invariant remains the same.
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
A concise explanation is: "There are two independent dimensions of variation, allocation and pricing, so 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
Failure and Concurrency Concerns
- Two entry panels claim the last compatible spot. The availability check and spot claim must be atomic at the floor or spot level. A global scan followed by a later assignment leaves a race window.
- Entry and exit touch the same spot concurrently. Serialize transitions for that spot and update the corresponding floor counter in the same critical section. Counts should be derived from the same mutation as occupancy.
- Payment fails at exit. Do not free the spot or close the ticket until payment succeeds or a documented alternative state records an unpaid exit. Retried payments need an idempotency key.
- A ticket is processed twice. Guard the ticket's lifecycle and reject a second exit after closure. This prevents double charging and double incrementing availability.
- Allocation policy returns an incompatible spot. Keep compatibility validation in
ParkingSpot/Flooras a domain guard; strategies choose among valid candidates but cannot bypass the invariant.
Extensibility
Design Trade-offs
Per-floor counters make capacity checks constant-time, but every path that changes occupancy must maintain them correctly; scanning spots is simpler but slower. A global lot lock makes the first implementation easy to prove, while floor- or spot-scoped locking improves concurrency and requires deterministic lock ordering. Strategy objects keep allocation and pricing open for change, but the compatibility matrix should remain domain data rather than being duplicated in each strategy.
If time allows, consider follow-up twists. Here is how the design handles common ones.
1. "How would you add EV charging spots?"
"Today the model has four SpotType values. For EV charging, 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 charging session tracking is required, 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?"
"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, and 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)?"
"The existing RateStrategy interface already isolates pricing. 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. 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 the requirements before drawing a single class. This keeps the process methodical and avoids 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 explicitly: "Synchronize spot assignment to handle concurrent entry panels." This ties the synchronization choice to a concrete invariant.
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) |
Test Your Understanding
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.
- Walk through a concrete scenario end-to-end after presenting the design. It validates the design and demonstrates how the classes collaborate, not just how they are drawn.