Understanding the Problem
đ What is a Parking Lot System? A parking lot system manages vehicle parking across multiple spots. When a vehicle enters, the system assigns an available spot matching the vehicle type and issues a ticket. When the vehicle exits, the system calculates the parking fee based on time spent and frees up the spot for the next customer.
Requirements
When you walk into the interview, youâll probably get something like this:
âDesign a parking lot system where different types of vehicles can park, and the system manages spot assignment and calculates fees upon exit.â
âDesign a parking lot system where different types of vehicles can park, and the system manages spot assignment and calculates fees upon exit.â
Before you start thinking about classes, you need to nail down exactly what youâre building. Spend a few minutes asking questions to turn this into something more concrete.
Clarifying Questions
Structure your questions around what the system does, how it handles mistakes, whatâs in scope, and what might change later.
Hereâs how the conversation might go:
You: âSo when a vehicle enters, the system assigns it a specific spot automatically?â Interviewer: âYes, the system assigns an available spot matching the vehicle type and issues a ticket.â
You: âSo when a vehicle enters, the system assigns it a specific spot automatically?â Interviewer: âYes, the system assigns an available spot matching the vehicle type and issues a ticket.â
You might be thinking, âwhat kind of parking lot assigns a specific spot when you drive in?â Fair question. Most real parking lots let you pick your own spot. But the assigned-spot version is how this problem gets asked in interviews because it forces you to design the allocation logic. I agree itâs goofy, but just roll with it.
You: âWhat types of vehicles does the system support? Just cars, or do we need motorcycles, trucks, that kind of thing?â Interviewer: âThree types. Motorcycles, regular cars, and large vehicles like SUVs or vans.â
You: âWhat types of vehicles does the system support? Just cars, or do we need motorcycles, trucks, that kind of thing?â
Interviewer: âThree types. Motorcycles, regular cars, and large vehicles like SUVs or vans.â
Good. Now you know the vehicle categories and that the system controls spot assignment.
You: âWhat happens when the vehicle enters? Do they get a ticket, a code, or something else to prove they parked there?â Interviewer: âThey get a ticket with a unique ID. Theyâll need that ticket to exit.â
You: âWhat happens when the vehicle enters? Do they get a ticket, a code, or something else to prove they parked there?â
Interviewer: âThey get a ticket with a unique ID. Theyâll need that ticket to exit.â
Now you know the entry flow.
You: âHow does pricing work? Is it hourly, flat rate, different rates for different vehicle types?â Interviewer: âKeep it simple. Hourly rate, same for all vehicles. Round up to the nearest hour and they pay when they leave.â
You: âHow does pricing work? Is it hourly, flat rate, different rates for different vehicle types?â
Interviewer: âKeep it simple. Hourly rate, same for all vehicles. Round up to the nearest hour and they pay when they leave.â
When the interviewer says âkeep it simple,â thatâs your signal to not over-engineer. Donât build a complex pricing engine with surge pricing and discounts unless they explicitly ask for it.
You: âWhat happens if the lot is full when someone tries to enter? Or if they try to exit with an invalid ticket?â Interviewer: âReject entry if thereâs no compatible spot available. For exit, return an error if the ticket is invalid or already used.â
You: âWhat happens if the lot is full when someone tries to enter? Or if they try to exit with an invalid ticket?â
Interviewer: âReject entry if thereâs no compatible spot available. For exit, return an error if the ticket is invalid or already used.â
Good error handling clarity.
You: âWhat if someone loses their ticket? Do we need to handle that case?â Interviewer: âGood question, but letâs keep it simple. Just assume they never lose their ticket for now.â
You: âWhat if someone loses their ticket? Do we need to handle that case?â
Interviewer: âGood question, but letâs keep it simple. Just assume they never lose their ticket for now.â
Interviewers notice when you ask about edge cases and failure modes. Questions like âwhat if they lose the ticket?â or âwhat happens when the system is full?â show youâre thinking about real-world problems. Donât go overboard with 20 edge cases, but getting into the habit of asking âwhat can go wrong?â signals mature engineering thinking.
You: âLast question. Whatâs out of scope? Are we worrying about payment processing, entrance gates, cameras, that kind of infrastructure?â Interviewer: âNo. Focus on the core logic. Spot assignment, ticket management, fee calculation. Skip the physical hardware, payment systems, and UI.â
You: âLast question. Whatâs out of scope? Are we worrying about payment processing, entrance gates, cameras, that kind of infrastructure?â
Interviewer: âNo. Focus on the core logic. Spot assignment, ticket management, fee calculation. Skip the physical hardware, payment systems, and UI.â
Perfect. Youâve scoped out what not to build.
Final Requirements
After that back-and-forth, youâd write this on the whiteboard:
Requirements:
1. System supports three vehicle types: Motorcycle, Car, Large Vehicle
2. When a vehicle enters, system automatically assigns an available compatible spot
3. System issues a ticket at entry.
4. When a vehicle exits, user provides ticket ID
- System validates the ticket
- Calculates fee based on time spent (hourly, rounded up)
- Frees the spot for next use
5. Pricing is hourly with same rate for all vehicles
6. System rejects entry if no compatible spot is available
7. System rejects exit if ticket is invalid or already used
Out of scope:
- Payment processing
- Physical gate hardware
- Security cameras or monitoring
- UI/display systems
- Reservations or pre-booking
Core Entities and Relationships
Now that requirements are clear, you need to figure out what objects make up your system. Look for nouns in your requirements, but donât turn every noun into a class. Some things are just data.
Letâs walk through the candidates:
Vehicle - This one seems obvious at first. Weâre parking vehicles, so we need a Vehicle class, right? But think about it. The vehicle is external to our system. We donât manage it, track it, or care about its state. We only need to know its type (motorcycle, car, large) to match it with a compatible spot. Thatâs a single piece of classification data, not an entity we need to model. Keep it as an enum, not a class.
ParkingSpot - This is a clear entity. A spot has an ID, a type to match vehicle types, and needs to track whether itâs occupied. This has both state and behavior, specifically the ability to mark itself as occupied or free.
Ticket - When a vehicle enters, we issue a ticket. That ticket is a record of the parking session. It holds the ticket ID, which spot was assigned, what type of vehicle, and when they entered. Unlike Vehicle (which is external), Ticket is internal state that our system creates and manages. It groups related data about an active parking session. Itâs worth modeling as a class even though itâs just a data holder.
ParkingLot - Something needs to orchestrate the whole system. When a vehicle enters, something needs to find an available spot, generate a ticket, and mark the spot occupied. When a vehicle exits, something needs to validate the ticket, calculate the fee, and free the spot. Thatâs the ParkingLot. Itâs the entry point and coordinator.
After filtering, weâre left with three entities:
The relationships are simple. ParkingLot owns a collection of ParkingSpots. ParkingLot creates Tickets when vehicles enter. ParkingLot tracks active tickets so it can validate them during exit.
Class Design
With our three entities identified, itâs time to define their interfaces. What state does each one hold, and what methods does it expose?
Weâll work top-down, starting with ParkingLot since itâs the entry point, then drilling into ParkingSpot and Ticket.
For each class, weâll ask two questions:
- What does this class need to remember to enforce the requirements (its state)?
- What operations does this class need to support (its methods)?
ParkingLot
ParkingLot is the orchestrator. Everything flows through it. Letâs derive its state from requirements:
First, who should track whether a spot is occupied? The spot itself or the parking lot? Letâs weigh the options.
Approach
One option is to add an occupied boolean field to ParkingSpot. You mark it true when someone parks there, and false when they leave.
findAvailableSpot(vehicleType)
requiredType = mapVehicleTypeToSpotType(vehicleType)
for spot in spots
if spot.isFree() and spot.spotType == requiredType
return spot
return null
The search logic is simple and direct. The spot âknowsâ its own state, you just ask it. The code also reads cleanly which is nice, âfind a spot thatâs free and matches the type.â
Challenges
The consideration with this approach is denormalization. Now occupancy lives in two places, both the flag on the spot and the activeTickets map (if a ticket references a spot, that spot is occupied). You must maintain the invariant that these two sources of truth stay synchronized. If you forget to update the flag somewhere, they diverge and you get bugs like assigning the same spot twice.
Thereâs also a design question about what occupancy represents. A spotâs size and ID are permanent characteristics that never changeâtheyâre part of what the spot is. But whether a spot is occupied? Thatâs not about the spot at all. Itâs about the parking lotâs current state: which ticket is assigned to which spot right now.
When a car gets a ticket at the entrance gate, that spot becomes occupied even though the car hasnât physically reached it yet. Same thing when leavingâthe spot stays occupied until the car exits the garage, not when it drives away from the spot. The occupancy is really about the ticket assignment, not the physical spot itself.
These are real concerns, but theyâre manageable with discipline. This approach works perfectly well if you prefer thinking of occupancy as an intrinsic property. In fact, the Amazon Locker problem uses exactly this patternâputting an occupied flag right on the Compartment entity.
Approach
You could skip storing occupancy entirely. A spot is occupied if and only if an active ticket references it. Compute this on demand:
findAvailableSpot(vehicleType)
requiredType = mapVehicleTypeToSpotType(vehicleType)
occupiedSpotIds = { ticket.spotId for ticket in activeTickets.values() }
for spot in spots
if spot.spotType == requiredType and spot.id not in occupiedSpotIds
return spot
return null
This is conceptually the cleanest. The ticketâs existence is the occupancy. Thereâs no redundancy at all so youâre not storing derived state, youâre computing it when needed.
Challenges
For a small parking lot (~200 spots, ~100 active tickets), this works fine. The computation is cheap, itâs a few microseconds per entry.
But youâre computing the same thing repeatedly. Every time someone enters, you scan all tickets to build the occupied set. And if you later add features that need concurrency (multiple entrances processing cars simultaneously), youâd need to lock the entire activeTickets map during both reads and writes. You canât have one thread iterating over tickets while another modifies the map.
This isnât a problem for the base requirements, but it creates headaches if you extend the system later. Despite these considerations, this is conceptually the cleanest approachâthereâs no denormalization at all. For small to medium systems, the simplicity might outweigh the concurrency and performance considerations.
Approach
To build on the previous approach, ParkingSpot can still stay a pure data holder and ParkingLot can track which spots are occupied using a separate Set.
class ParkingLot:
- spots: List<ParkingSpot>
- occupiedSpotIds: Set<String> // Index for occupancy
- activeTickets: Map<String, Ticket>
- hourlyRateCents: long
class ParkingSpot:
- id: String // Intrinsic
- spotType: SpotType // Intrinsic
findAvailableSpot(vehicleType)
requiredType = mapVehicleTypeToSpotType(vehicleType)
for spot in spots
if spot.spotType == requiredType and spot.id not in occupiedSpotIds
return spot
return null
Yes, the Set is technically redundant with the ticket data. You could derive occupancy from tickets. But the Set serves as a maintained index. Like a database index, it must stay in sync with the source data, but the trade-off is worth it.
We get the small win on performance, although, again, this doesnât matter at this size of N. But the Set also gives you a clean concurrency boundary. When we add multi-entrance support later (see the extensibility section), this becomes important. You can lock just the Set for atomic spot claiming, rather than locking the entire tickets map. Weâll explore this in the concurrency deep dive.
Weâre choosing the indexed approach for this problem. It keeps occupancy tracking centralized, gives us O(1) lookups, and sets us up well for the concurrency patterns weâll discuss in the extensibility section. But as we discussed above, the other approaches are equally valid for different priorities.
In the Amazon Locker problem, we track occupancy differentlyâusing an occupied boolean flag directly on the Compartment entity instead of maintaining a Set in the orchestrator.
Why the difference? In Amazon Locker, occupancy represents physical presence. A package is physically placed in the compartment, then the token is generated. Even after the token expires, the package is still physically there. Thatâs intrinsic state.
In Parking Lot, occupancy represents assignment. The ticket is issued at the gate (creating the assignment) before the car physically reaches the spot. The spot becomes âoccupiedâ the moment we assign it, not when the car parks. Thatâs relational state.
Both approaches are defensible for both problems. Weâre making different choices based on how we think about the domain. What matters is understanding the tradeoffs and being able to defend your reasoning, not memorizing which pattern to use when.
When deciding where state belongs, ask: âIs this a property of the entity itself, or a relationship managed by the system?â
- Intrinsic (entity owns): ID, size, physical status like BROKEN
- Relational (orchestrator owns): âcurrently assigned to ticket Xâ, âreserved by user Yâ
Occupancy is relational, itâs derived from âa ticket references this spot.â The orchestrator manages tickets, so it should manage occupancy. Whether you compute it on demand or maintain an index depends on your performance and concurrency needs.
That said, this distinction isnât absolute. In Amazon Locker, we treat occupancy as physical state (a flag on the entity). Here, weâre treating it as relational (a Set in the orchestrator). Both approaches work for both problems. The distinction is a helpful mental model, not a hard rule.
This gives us:
class ParkingLot:
- spots: List<ParkingSpot>
- occupiedSpotIds: Set<String>
- activeTickets: List<Ticket>
- hourlyRateCents: long
Letâs break down why each field is needed and why it belongs in ParkingLot:
Why spots belongs here. The lot owns the collection of all parking spots. When a vehicle enters, ParkingLot scans this list to find an available compatible spot. This is the central resource that the orchestrator manages. Spots could live elsewhere (maybe a separate SpotManager class), but that just adds indirection. The lot manages spots, so the lot holds spots.
Why activeTickets as a list. During exit, our system receives a ticket ID string. We need to look it up to validate it exists and get the entry time for fee calculation. Weâll start with a simple list here and refine this in the implementation section.
Why hourlyRateCents belongs here. Pricing is a system-level policy that the lot enforces. Different parking lots might charge different rates. Storing it here lets us instantiate multiple ParkingLot objects with different rates if needed. The alternative is hardcoding it globally, which makes testing harder. We could also pass the rate into exit() each time, but then weâre forcing callers to know about pricing, which breaks encapsulation.
Avoid using floating-point types for money. Floats canât represent decimal fractions exactly - they use binary fractions internally, so values like 0.1 canât be stored precisely. This leads to tiny errors that accumulate in calculations. Store the smallest unit (cents, pennies) as an integer instead. $5.47 becomes 547 cents. All calculations stay exact, and you only convert to dollars for display. See 0.30000000000000004.com for examples of this issue across languages.
Now for operations. What actions does the outside world need to perform?
Thatâs it. Two methods. The entire public API.
class ParkingLot:
- spots: List<ParkingSpot>
- occupiedSpotIds: Set<String>
- activeTickets: List<Ticket> // We'll refine this to a Map during implementation
- hourlyRateCents: long
+ ParkingLot(spots, hourlyRateCents)
+ enter(vehicleType) -> Ticket
+ exit(ticketId) -> long
The constructor takes a list of spots (configured externally) and a rate. enter takes a vehicle type and returns the ticket if successful, throws an error if no spots available. exit takes a ticket ID and returns the fee in cents, throws an error if the ticket is invalid.
Some candidates add a getAvailableSpots() or getParkingStatus() method thinking they need to expose internal state. Unless the requirements explicitly say you need to query the lotâs status, there is no need to add these methods. They violate encapsulation and arenât needed for the core workflow. If the interviewer asks later about monitoring or dashboards, you can add them then.
ParkingSpot
ParkingSpot represents one physical parking space. From requirements:
State:
class ParkingSpot:
- id: String
- spotType: SpotType
For operations:
class ParkingSpot:
- id: String
- spotType: SpotType
+ ParkingSpot(id, spotType)
+ getSpotType() -> SpotType
+ getId() -> String
ParkingSpot is deliberately simple. Itâs a pure data holder representing the physical properties of a parking space. It doesnât know about vehicles, tickets, pricing, or even whether itâs occupiedâthatâs all managed by ParkingLot.
The enums:
enum SpotType:
MOTORCYCLE
CAR
LARGE
enum VehicleType:
MOTORCYCLE
CAR
LARGE
We have two separate enums (SpotType and VehicleType) even though they have the same values. This keeps them semantically distinct. A spot type is not the same concept as a vehicle type, even if they happen to use the same labels. If requirements later say âmotorcycles can use car spots if motorcycle spots are full,â having separate enums makes that logic clearer.
Ticket
Ticket is a record of a parking session. From requirements:
State:
class Ticket:
- id: String
- spotId: String
- vehicleType: VehicleType
- entryTimeMs: long
Why spotId as a string, not a reference to ParkingSpot? Tickets are records, not navigational objects. They shouldnât reach into the domain model. Storing just the ID keeps them simple and prevents tickets from accidentally calling methods on spots. This is the Law of Demeter in action.
Why entryTimeMs as a long (timestamp in milliseconds)? We need to calculate time spent, which means we need to do arithmetic with time values. Weâre using a simple long here to keep the pseudocode language-agnostic. In real code, you could also use your languageâs native time type (Javaâs Instant, Pythonâs datetime, JavaScriptâs Date, etc.) which handle timezone and duration calculations properly.
Now we need to decide where fee calculation logic belongs. This is a common design decision that trips up many candidates.
Approach
Some candidates see that Ticket has the entry time and think âthe ticket should calculate its own fee.â They add a calculateFee() method directly on the Ticket class.
class Ticket:
- id: String
- spotId: String
- vehicleType: VehicleType
- entryTime: long
- hourlyRateCents: long // Store rate in ticket
long calculateFee(long exitTime) -> long
This seems reasonable on the surface. The ticket âownsâ the entry time, so shouldnât it own the calculation that uses it?
Challenges
This violates Single Responsibility Principle. The Ticket is now both a record of a parking session AND a pricing calculator. It has two reasons to change: if we change what data a ticket stores, or if we change how fees are calculated.
More importantly, pricing is a business policy, not a property of a ticket. Different parking lots charge different rates. Rates might change over time. Future requirements might add surge pricing, discounts, or per-vehicle-type rates. All of that complexity would pile into the Ticket class.
Youâve also made Ticket mutable. Now it needs to store the hourly rate, but what if rates change while someone is parked? Do you store the rate that was active when they entered? Thatâs more state to track.
Approach
Keep Ticket as a pure data holder with no behavior. Put the fee calculation logic in ParkingLot, where all the other business rules live.
class Ticket:
- id: String
- spotId: String
- vehicleType: VehicleType
- entryTime: long
class ParkingLot:
- hourlyRateCents: long
computeFee(entryTime, exitTime)
durationMillis = exitTime - entryTime
durationHours = durationMillis / (1000 * 60 * 60)
if durationMillis % (1000 * 60 * 60) > 0
durationHours++ // Any partial hour rounds up, so 5 minutes becomes 1 hour
return durationHours * hourlyRateCents
Ticket stays immutable. Itâs a simple record with no business logic. ParkingLot orchestrates everything, including pricing. When you call exit(), ParkingLot gets the entry time from the ticket and calculates the fee.
Benefits
This follows Separation of Concerns. Business rules live in the orchestrator, data records stay simple. All pricing logic is in one place, making it easy to test, modify, and understand.
When pricing gets more complex (per-type rates, discounts, time-of-day pricing), all changes happen in one place. The Ticket class never needs to change. You can test computeFee() independently with different time inputs without creating Ticket objects.
This also makes the ownership clear: the parking lot enforces pricing policy, tickets are just receipts.
Approach
If we were building software to be used by multiple parking lots with different pricing rules, we would extract pricing into its own abstraction using the Strategy pattern.
interface PricingStrategy
calculateFee(ticket, exitTime)
class HourlyPricing implements PricingStrategy
- hourlyRateCents
calculateFee(ticket, exitTime)
// Same logic as before
class DynamicPricing implements PricingStrategy
calculateFee(ticket, exitTime)
// Complex logic: surge pricing, time of day, etc.
class ParkingLot
- pricingStrategy
exit(ticketId)
ticket = activeTickets[ticketId]
fee = pricingStrategy.calculateFee(ticket, currentTime())
// ... rest of exit logic
Now you can swap pricing strategies at runtime or have different strategies for different parking lots without changing ParkingLot at all.
This approach makes sense when youâre building software for multiple parking lots that need completely different pricing models, or when pricing rules are so complex they deserve their own class and you need to swap strategies at runtime based on conditions. But our requirements say âsimple hourly pricing for all vehicles.â Building a Strategy pattern for a single, simple pricing rule violates YAGNI. Youâre adding abstraction for future requirements that donât exist.
The âgoodâ approach (method in ParkingLot) is the right answer here. If the interviewer later asks âhow would you support different pricing models?â, thatâs when you explain the Strategy pattern as an extension. But donât build it upfront.
Given we are designing for a single parking lot, we will go with the Good approach. Ticket has no behavior beyond getters. Itâs a pure data holder:
class Ticket:
- id: String
- spotId: String
- vehicleType: VehicleType
- entryTime: long
+ Ticket(id, spotId, vehicleType, entryTime)
+ getId() -> String
+ getSpotId() -> String
+ getVehicleType() -> VehicleType
+ getEntryTime() -> long
All fields are read-only after construction. Once a ticket is issued, it never changes. This makes tickets immutable value objects, which is exactly what you want for records.
Final Class Design
Hereâs how the system fits together: ParkingLot acts as the orchestrator, receiving entry and exit requests, tracking which spots are occupied, finding available spots, generating tickets, and calculating fees. ParkingSpot is a pure data holder representing physical parking spaces (intrinsic properties only). Ticket is an immutable record created at entry that captures the parking session details needed for fee calculation at exit.
class ParkingLot:
- spots: List<ParkingSpot>
- occupiedSpotIds: Set<String>
- activeTickets: Map<string, Ticket>
- hourlyRateCents: long
+ ParkingLot(spots, hourlyRateCents)
+ enter(vehicleType) -> Ticket
+ exit(ticketId) -> long
class ParkingSpot:
- id: String
- spotType: SpotType
+ ParkingSpot(id, spotType)
+ getSpotType() -> SpotType
+ getId() -> String
class Ticket:
- id: String
- spotId: String
- vehicleType: VehicleType
- entryTime: long
+ Ticket(id, spotId, vehicleType, entryTime)
+ getId() -> String
+ getSpotId() -> String
+ getVehicleType() -> VehicleType
+ getEntryTime() -> long
enum SpotType:
MOTORCYCLE
CAR
LARGE
enum VehicleType:
MOTORCYCLE
CAR
LARGE
The design maintains a clear separation of concerns: orchestration, relational state, and business rules in ParkingLot; intrinsic properties in ParkingSpot; and session data in Ticket.
Implementation
With the class design locked in, we need to implement the core methods. Before starting, check with your interviewer. Some want working code in a specific language, others want pseudocode, some just want you to talk through it. Weâll use pseudocode here, but the full implementations in multiple languages are at the end.
For each method, weâll follow a pattern:
- Define the core logic - The happy path that fulfills the requirement
- Handle edge cases - Invalid inputs, boundary conditions, unexpected states
The most interesting methods are enter and exit on ParkingLot. Those are where the orchestration happens.
ParkingLot
Before we implement the methods, letâs refine one design decision. In our class design, we kept activeTickets as a list. But during exit, we need to look up a ticket by its ID. While we could scan through the list, using a Map<String, Ticket> makes the âlookup by IDâ intent explicit and cleaner. The performance difference is negligible at parking lot scale, but the map makes the code more readable.
I ran a quick benchmark to prove that it doesnât matter from a time complexity perspective. With 200 tickets, map lookup averaged 0.12 microseconds and list scan averaged 1.93 microseconds. The map is technically 16x faster, but weâre talking about a 1.8 microsecond difference. Thatâs 0.0000018 seconds. When someone exits the parking lot, the time to physically drive through the gate, or the network latency to process their payment, will dwarf this difference by orders of magnitude.
Both are correct if you can explain your reasoning. The map is slightly cleaner because it makes the âlookup by IDâ intent explicit, but the performance argument is irrelevant at this scale and it can be a fun thing to callout in an interview.
class ParkingLot:
- spots: List<ParkingSpot>
- occupiedSpotIds: Set<String>
- activeTickets: Map<String, Ticket> // Changed from List to Map
- hourlyRateCents: long
Letâs start with enter. This is where vehicles arrive and get assigned a spot.
Core logic:
- Find an available spot that matches the vehicle type
- If no spot found, throw an error
- Add the spot ID to occupiedSpotIds
- Generate a unique ticket with spot ID, vehicle type, and current timestamp
- Store the ticket in activeTickets map
- Return the ticket
Edge cases:
- No available spots for this vehicle type
- Invalid vehicle type (though the enum prevents this)
Hereâs the pseudocode. Weâll assume standard utility methods generateUniqueId() (returns a UUID string) and getCurrentTimestamp() (returns Unix time in milliseconds):
enter(vehicleType)
spot = findAvailableSpot(vehicleType)
if spot == null
return error
occupiedSpotIds.add(spot.id)
ticket = createTicket(
generateId(),
spot.id,
vehicleType,
currentTime()
)
activeTickets[ticket.id] = ticket
return ticket
The flow is straightforward. Find a spot, mark it occupied in our Set, create the ticket, store it, return it. If no spot exists, we throw an error before changing any state.
Now exit. This is where vehicles leave and pay.
Core logic:
- Look up the ticket by ID in activeTickets map
- If not found, throw an error (invalid or already used)
- Calculate fee based on entry time and current time
- Remove the spot ID from occupiedSpotIds (frees the spot)
- Remove the ticket from activeTickets (prevents double exit)
- Return the fee
Edge cases:
- Ticket ID doesnât exist (invalid or already used)
- Ticket ID is null or empty
- Time calculation edge cases (what if they stayed 0 minutes? Still charge for 1 hour per âround upâ rule)
exit(ticketId)
if ticketId == null
return error
ticket = activeTickets[ticketId]
if ticket == null
return error
exitTime = currentTime()
fee = computeFee(ticket.entryTime, exitTime)
occupiedSpotIds.remove(ticket.spotId)
activeTickets.remove(ticketId)
return fee
We validate the ticket exists, calculate the fee, free the spot (by removing from occupiedSpotIds), and remove the ticket. The ticket removal is important. It prevents someone from exiting twice with the same ticket. After exit, the ticket ID becomes invalid.
Weâre not distinguishing between âticket never existedâ and âticket already used.â Both return the same error. If you wanted to provide more specific feedback, you could track used tickets in a separate set. But for interview scope, treating both as âinvalid ticketâ is simpler and good enough.
Letâs look at some of the key helper methods, starting with findAvailableSpot which is called in the first line of the enter method.
findAvailableSpot(vehicleType)
requiredSpotType = mapVehicleTypeToSpotType(vehicleType)
for spot in spots
if spot.spotType == requiredSpotType and spot.id not in occupiedSpotIds
return spot
return null
mapVehicleTypeToSpotType(vehicleType)
if vehicleType == MOTORCYCLE
return MOTORCYCLE
if vehicleType == CAR
return CAR
if vehicleType == LARGE
return LARGE
return error
No need to overcomplicate this. We iterate over all spots, check if they match the required spot type and arenât in our occupiedSpotIds set. The Set gives us O(1) lookup for each spot. If we donât find any available spots, we return null.
Some candidates try to be clever and add complex allocation logic like âprefer spots near the entrance.â Unless the requirements mention this, donât add it. Youâre burning time on features nobody asked for. If the interviewer wants smarter allocation, theyâll ask as an extension question or you can ask them whether they want you to implement it.
What about computeFee?
computeFee(entryTime, exitTime)
durationMillis = exitTime - entryTime
durationHours = durationMillis / (1000 * 60 * 60)
// Round up to nearest hour (5 minutes becomes 1 hour)
if durationMillis % (1000 * 60 * 60) > 0
durationHours++
return durationHours * hourlyRateCents
We calculate time spent, convert to hours, round up (any partial hour counts as a full hour), and multiply by the rate. Because we round up, someone who parks for 5 minutes gets charged for 1 hour â no separate minimum charge logic needed.
ParkingSpot
The methods here are trivialâjust getters:
getSpotType()
return spotType
getId()
return id
ParkingSpot is a pure data holder. No state management, no occupancy tracking.
Ticket
Ticket is all getters:
getId()
return id
getSpotId()
return spotId
getVehicleType()
return vehicleType
getEntryTime()
return entryTime
Pure data holder. No behavior.
Complete Code Implementation
While most companies only require pseudocode during interviews, some do ask for full implementations of at least a subset of the classes or methods. Below is a complete working implementation in common languages for reference.
import uuid
import time
from typing import List, Dict, Set
class ParkingLot:
def __init__(self, spots: List, hourly_rate_cents: int):
self._spots = spots
self._active_tickets: Dict[str, Ticket] = {}
self._occupied_spot_ids: Set[str] = set()
self._hourly_rate_cents = hourly_rate_cents
def enter(self, vehicle_type):
spot = self._find_available_spot(vehicle_type)
if spot is None:
raise Exception(f"No available spots for vehicle type {vehicle_type}")
self._occupied_spot_ids.add(spot.get_id())
ticket_id = str(uuid.uuid4())
entry_time = int(time.time() * 1000)
ticket = Ticket(ticket_id, spot.get_id(), vehicle_type, entry_time)
self._active_tickets[ticket_id] = ticket
return ticket
def exit(self, ticket_id: str) -> int:
if ticket_id is None or ticket_id == "":
raise Exception("Invalid ticket ID")
ticket = self._active_tickets.get(ticket_id)
if ticket is None:
raise Exception("Ticket not found or already used")
exit_time = int(time.time() * 1000)
fee = self._compute_fee(ticket.get_entry_time(), exit_time)
self._occupied_spot_ids.discard(ticket.get_spot_id())
del self._active_tickets[ticket_id]
return fee
def _find_available_spot(self, vehicle_type):
required_spot_type = self._map_vehicle_type_to_spot_type(vehicle_type)
for spot in self._spots:
if spot.get_id() not in self._occupied_spot_ids and spot.get_spot_type() == required_spot_type:
return spot
return None
def _map_vehicle_type_to_spot_type(self, vehicle_type):
if vehicle_type == VehicleType.MOTORCYCLE:
return SpotType.MOTORCYCLE
if vehicle_type == VehicleType.CAR:
return SpotType.CAR
if vehicle_type == VehicleType.LARGE:
return SpotType.LARGE
raise Exception("Unknown vehicle type")
def _compute_fee(self, entry_time: int, exit_time: int) -> int:
duration_millis = exit_time - entry_time
duration_hours = duration_millis // (1000 * 60 * 60)
if duration_millis % (1000 * 60 * 60) > 0:
duration_hours += 1
return duration_hours * self._hourly_rate_cents
Verification
Letâs trace through a scenario to verify the state management works correctly. This catches bugs before your interviewer finds them.
ParkingLot has 3 spots: spot A (MOTORCYCLE), spot B (CAR), spot C (LARGE). occupiedSpotIds is empty. No active tickets. Hourly rate is $5 (500 cents).
Vehicle enters:
Initial: spots=[A, B, C], occupiedSpotIds={}, activeTickets={}
findAvailableSpot(CAR) â finds spot B (type matches, not in occupiedSpotIds)
occupiedSpotIds.add("B") â set now {"B"}
Generate ticket: id="T123", spotId="B", vehicleType=CAR, entryTime=1000000
activeTickets.put("T123", ticket) â map now {"T123" â ticket}
Return ticket T123
State: occupiedSpotIds={"B"}, activeTickets has T123
The spot is marked occupied in our Set and the ticket is stored.
Vehicle exits 2.5 hours later:
activeTickets.get("T123") â ticket found
exitTime = 1000000 + (2.5 hours in millis) = 10000000
computeFee(1000000, 10000000):
- duration = 2.5 hours
- round up â 3 hours
- fee = 3 * 500 = 1500 cents
occupiedSpotIds.remove("B") â set now empty
activeTickets.remove("T123") â map now empty
Return 1500 cents
State: occupiedSpotIds={}, activeTickets={}
Fee correctly rounds up 2.5 to 3 hours. Spot is freed (removed from Set). Ticket is removed.
Try to exit again with same ticket:
activeTickets.get("T123") â null (already removed)
throw Error("Ticket not found or already used")
Double exit is prevented.
Try to enter when lot is full:
All CAR spots are in occupiedSpotIds
findAvailableSpot(CAR) â returns null
throw Error("No available spots for vehicle type CAR")
Entry is rejected without changing state.
This verifies the core workflows, state transitions, and error handling all work correctly.
Extensibility
If thereâs time left after implementation, interviewers often ask âwhat ifâ questions to see if your design can evolve cleanly. You typically wonât implement these changes, just explain where theyâd fit.
The depth of this section depends on your target level. Junior candidates often skip it. Mid-level candidates get one or two simple questions. Senior candidates might get several in a row.
If youâre a junior engineer, feel free to skip this section and stop reading here! Only carry on if youâre curious about the more advanced concepts.
Below are the most common ones for parking lot systems, with more detail than youâd need in an actual interview.
1. âHow would you extend this to a multi-floor parking garage?â
Our current design assumes a single floor with a flat list of spots. But what about a 10-floor garage at a shopping mall with thousands of spots? The interviewer wants to see if your design can scale without a complete rewrite.
âThe main change is introducing a ParkingFloor entity between ParkingLot and ParkingSpot. Each floor owns a collection of spots, and ParkingLot owns a collection of floors. The floor becomes part of the spotâs identity, so spot IDs become something like â3-A15â for floor 3, section A, spot 15.â
âThe main change is introducing a ParkingFloor entity between ParkingLot and ParkingSpot. Each floor owns a collection of spots, and ParkingLot owns a collection of floors. The floor becomes part of the spotâs identity, so spot IDs become something like â3-A15â for floor 3, section A, spot 15.â
class ParkingLot:
- floors: List<ParkingFloor>
- activeTickets: Map<String, Ticket>
- hourlyRateCents
class ParkingFloor:
- floorNumber
- spots: List<ParkingSpot>
+ getAvailableSpotCount(spotType) -> int
+ findAvailableSpot(spotType) -> ParkingSpot
The spot-finding logic now has options. The simplest approach is to iterate through floors in order and return the first available spot:
findAvailableSpot(vehicleType)
requiredType = mapVehicleTypeToSpotType(vehicleType)
for floor in floors
spot = floor.findAvailableSpot(requiredType)
if spot != null
return spot
return null
But with 10 floors, you might want smarter allocation. You could even use a Strategy pattern to implement different allocation strategies based on occupancy or time of day.
1. Fill lower floors first â Keeps customers closer to the entrance/exit. This is what the simple iteration does. 2. Balance across floors â Spread vehicles evenly so no single floor gets congested. Track spot counts per floor and prefer floors with more availability. 3. Proximity to destination â In a mall garage, different floors might be closer to different stores. If someone says âIâm going to the food court,â assign them to floor 4 which connects directly.
findAvailableSpot(vehicleType)
requiredType = mapVehicleTypeToSpotType(vehicleType)
// Find floor with most available spots of this type
bestFloor = null
maxAvailable = 0
for floor in floors
available = floor.getAvailableSpotCount(requiredType)
if available > maxAvailable
maxAvailable = available
bestFloor = floor
if bestFloor == null
return null
return bestFloor.findAvailableSpot(requiredType)
The Ticket class doesnât change at all â it still stores the spotId, which now includes floor information implicitly (e.g., â3-A15â). The exit flow is identical: look up ticket, compute fee, free spot by ID, though you could also add a Floor state to the Ticket class to make it more explicit if youâd like.
2. âHow would you add different pricing for different vehicle types?â
Right now we have one hourly rate for everyone. What if motorcycles are $3/hour, cars are $5/hour, and large vehicles are $8/hour?
âThere are two ways to do this. The simple way is to add a map from VehicleType to rate in ParkingLot. Store three rates instead of one. Then in
computeFee, look up the rate based on the vehicle type from the ticket. The more sophisticated way, if we expect complex pricing rules, is to introduce a PricingStrategy interface with different implementations. But thatâs overkill unless we have very complex rules like surge pricing or discounts. For just three different rates, the map is simpler.â
âThere are two ways to do this. The simple way is to add a map from VehicleType to rate in ParkingLot. Store three rates instead of one. Then in computeFee, look up the rate based on the vehicle type from the ticket.
The more sophisticated way, if we expect complex pricing rules, is to introduce a PricingStrategy interface with different implementations. But thatâs overkill unless we have very complex rules like surge pricing or discounts. For just three different rates, the map is simpler.â
class ParkingLot:
- spots: List<ParkingSpot>
- activeTickets: Map<String, Ticket>
- hourlyRates: Map<VehicleType, long> // Change: map instead of single rate
computeFee(entryTime, exitTime, vehicleType)
durationHours = calculateDuration(entryTime, exitTime)
rate = hourlyRates[vehicleType] // Look up rate by vehicle type
return durationHours * rate
The signature of computeFee changes to take vehicle type (which we get from the ticket). The rest stays the same.
3. âHow would you handle multiple entrances with concurrent access?â
If a large parking lot has multiple entrances, you can have two vehicles trying to enter at the exact same time. This creates a classic race condition where both threads could find the same spot available and try to assign it. Then both cars will have tickets for the same spot. Awkward!
The window between checking if a spot is available and adding it to occupiedSpotIds is where the race happens. Letâs look at how to fix this.
Approach
Lock the entire enter() method so only one vehicle can enter at a time. Hereâs how youâd implement this across different languages:
from threading import Lock
class ParkingLot:
def __init__(self):
self._lock = Lock()
self._occupied_spot_ids = set()
def enter(self, vehicle_type):
# Acquire lock, automatically released when exiting 'with' block
with self._lock:
# Find spot, add to occupied set, create ticket
spot = self._find_available_spot(vehicle_type)
if spot is None:
raise Exception("No available spots")
self._occupied_spot_ids.add(spot.id)
# ... rest of enter logic
return ticket
The concept is the same across languages: serialize all entrance requests. When the North entrance is processing a vehicle, South and East wait. When North finishes, one of the waiting entrances acquires the lock and proceeds.
This is simple, correct, and sufficient for most parking lots. If each entry takes 100 milliseconds (find spot, generate ticket, update state), you can process 10 vehicles per second. A parking lot with 200 spots turning over once every 2 hours needs roughly 0.03 vehicles per second on average. You have 300x headroom.
Approach
Instead of locking the entire method, use a read-write lock. Multiple threads can read occupiedSpotIds concurrently when finding spots, but writes require exclusive access. This gives better concurrency than a single mutex while remaining correct.
from threading import RLock
class ParkingLot:
def __init__(self):
# Python's threading module doesn't have RWLock built-in
# Use RLock for simplicity (or use a third-party RWLock)
self._lock = RLock()
self._occupied_spot_ids = set()
def _find_available_spot(self, vehicle_type):
with self._lock: # Acquire lock for read
for spot in self._spots:
if (spot.spot_type == vehicle_type and
spot.id not in self._occupied_spot_ids):
return spot
return None
def enter(self, vehicle_type):
while True:
spot = self._find_available_spot(vehicle_type)
if spot is None:
raise Exception("No available spots")
with self._lock: # Acquire lock for write
if spot.id not in self._occupied_spot_ids:
self._occupied_spot_ids.add(spot.id)
# ... create ticket
return ticket
# Spot was claimed, retry
The key insight: findAvailableSpot() reads from occupiedSpotIds, which means it must be synchronized with writes. A read-write lock (RWMutex in Go, ReaderWriterLockSlim in C#, shared_mutex in C++) allows multiple concurrent readers while ensuring exclusive access for writers.
The pattern is: acquire a read lock when searching for spots, then acquire a write lock to claim. Since another thread might claim the spot between your read and write, you double-check inside the write lock and retry if needed.
This gives better concurrency than a single mutex because multiple entrances can search for spots simultaneously. The write lock is only held briefly for the actual claim operation.
This discussion assumes synchronous, in-memory operations. If youâre using async operations (database queries, API calls), single-threaded languages like Node.js can still have race conditions. The event loop can interleave requests at each await point, creating the same bug. In that case, youâd handle synchronization at the database level using transactions with proper isolation levels (SELECT FOR UPDATE in SQL) or application-level distributed locks (Redis, ZooKeeper). This is something discussed in System Design interviews, not Low Level Design typically.
So, as a candidate, you might respond with:
âWith multiple entrances, we have a race condition where two vehicles could be assigned the same spot. The simplest correct solution is to synchronize the entire
enter()method, which serializes all entrance requests. This is sufficient for most parking lots. If we needed higher concurrency, we could use atomic check-and-add on theoccupiedSpotIdsSet with retry logic. For a parking lot with 3-5 entrances and typical traffic, method-level synchronization is the right choiceâitâs simple, correct, and performance isnât an issue.â
âWith multiple entrances, we have a race condition where two vehicles could be assigned the same spot. The simplest correct solution is to synchronize the entire enter() method, which serializes all entrance requests. This is sufficient for most parking lots. If we needed higher concurrency, we could use atomic check-and-add on the occupiedSpotIds Set with retry logic. For a parking lot with 3-5 entrances and typical traffic, method-level synchronization is the right choiceâitâs simple, correct, and performance isnât an issue.â
What is Expected at Each Level?
So what am I looking for at each level?
Junior
At the junior level, Iâm checking whether you can break down the problem and implement a working system. You should identify the need for something to represent spots, tickets, and an orchestrator. Your enter flow should find a spot, mark it occupied, and return a ticket. Your exit flow should calculate a fee and free the spot. Basic error handling matters: reject entry when full, reject invalid tickets. Itâs fine if you need hints on where to put pricing logic. What matters is building something that works.
Mid-level
For mid-level candidates, I expect cleaner separation of concerns without much guidance. ParkingLot orchestrates, ParkingSpot holds spot properties, Ticket is a simple data holder. You should recognize that Vehicle doesnât need to be a class (itâs external, just a classification label). Handle the key edge cases: full lot, invalid tickets, double exits. You should justify design decisions when asked: why is activeTickets a map? Why is pricing in ParkingLot instead of on Ticket? If time allows, discuss at least one extension like multi-floor or per-type pricing.
Senior
Senior candidates should produce a design that demonstrates systems thinking. Class boundaries should be obvious without deliberation. You should proactively discuss trade-offs: for example, the occupied flag is controlled denormalization, tickets in a map enable O(1) lookup, and separate enums for SpotType and VehicleType allow for future flexibility or any other reasonable trade-off. I expect you to catch edge cases yourself. For extensibility, you should be able to discuss multiple approaches, explaining the simple solution first, and then when youâd reach for patterns like Strategy. Strong candidates finish early and can discuss how the design evolves for multi-floor garages or concurrent access.