Code a parking lot with tickets and fees
Open in the interactive app: practice, quiz, mock interview →In one minute
- A parking lot is a resource allocator with a cash register attached. The hard part is not the class list; it is guaranteeing that one spot never goes to two vehicles and that the fee is a pure function of facts you recorded at entry.
- Model the domain with a small set of nouns (lot, level, spot, vehicle, ticket) and push the parts that change (which spot to pick, how to price) behind interfaces. Those two strategies are where requirements churn.
- Make time an injected dependency. A fee computed from
datetime.now()inside the pricing code cannot be tested, replayed or audited. - Treat "park" as one atomic step: choose a spot, mark it taken and issue a ticket under the same lock or transaction. Two entry gates racing for the last spot is the canonical bug.
- The shape is contention over a pool of identical-ish resources. The same design reappears as seat holds in ticketing, IP address leases in DHCP, and GPU slots in a training scheduler.
Requirements
Functional:
- The lot has several levels; each level has spots of three sizes: small (motorcycles), medium (cars) and large (vans, buses).
- A vehicle may use a spot of its size or larger. The system picks the spot, issues a ticket at the entry gate and refuses entry when nothing fits.
- At exit, the driver presents the ticket, pays a fee based on vehicle size and duration, and the spot becomes free.
- Display boards show free spots per size and level.
Non-functional:
- No double allocation, even with several entry gates working at once.
- Fees are reproducible: given the ticket and the exit time, anyone can recompute the same amount.
- Entry decision in well under a second; the barrier arm is the slow part, not the software.
Out of scope for the interview: online reservations, monthly passes, EV charging and licence-plate recognition. Mention them, park them in Extensibility, and move on.
Entities and relationships
Start with nouns and ask which ones hold state that changes.
- ParkingLot is the façade the gates call. It owns levels, the active tickets and the two strategies.
- Level owns its spots and knows its free counts. Levels exist mainly for display boards and for allocation preference (fill the ground floor first).
- ParkingSpot has an id, a size and an occupant (a ticket id or nothing). It is the unit of contention.
- Vehicle is a value: plate and size. Resist a
Car,Bus,Motorcyclehierarchy; the only behaviour that differs is size, and an enum carries that. Know the classic variant, though: in the version in Cracking the Coding Interview and the system-design-primer solution, a bus needs five consecutive large spots. That changes allocation from "pop one free id" to "find a free run of length k in a row", and a vehicle now holds a list of spots. If the interviewer adds that rule, it belongs in the allocation strategy and the ticket, still not in a subclass. - Ticket links a vehicle to a spot with an entry time and a status (active, paid, closed). It is the record you will be asked to defend in a billing dispute, so make it immutable apart from status.
- AllocationStrategy picks a spot for a vehicle. PricingStrategy turns a ticket and an exit time into money. Clock supplies the time.
Relationships: a lot is composed of levels, a level of spots (they do not exist independently). A ticket references a spot and a vehicle but does not own them. The lot aggregates active tickets keyed by ticket id, plus a set of plates currently inside so the same car cannot be admitted twice.
Class diagram
classDiagram
class ParkingLot {
-levels: Level[]
-active: Map~TicketId, Ticket~
+park(plate, size) Ticket
+unpark(ticketId) Money
+freeCount(size) int
}
class Level {
+number: int
-freeBySize: Map~Size, SpotId[]~
}
class ParkingSpot {
+id: SpotId
+size: Size
+occupant: TicketId
}
class Ticket {
+id: TicketId
+plate: string
+size: Size
+spotId: SpotId
+entryTime: int
+status: TicketStatus
}
class AllocationStrategy {
<<interface>>
+pick(levels, size) ParkingSpot
}
class PricingStrategy {
<<interface>>
+fee(ticket, exitTime) Money
}
class Clock {
<<interface>>
+now() int
}
ParkingLot "1" *-- "many" Level
Level "1" *-- "many" ParkingSpot
ParkingLot "1" o-- "many" Ticket
Ticket --> ParkingSpot
ParkingLot --> AllocationStrategy
ParkingLot --> PricingStrategy
ParkingLot --> Clock
SmallestFit ..|> AllocationStrategy
HourlyRate ..|> PricingStrategyKey flows
Entry and exit share the same lock on the lot. Pricing happens outside the critical section in a real system, because it is a pure function of the ticket and the exit time.
sequenceDiagram
participant D as Driver
participant G as EntryGate
participant L as ParkingLot
participant A as AllocationStrategy
participant X as ExitGate
participant P as PricingStrategy
D->>G: arrive (plate, size)
G->>L: park(plate, size)
L->>L: lock
L->>A: pick(levels, size)
A-->>L: spot L1-007
L->>L: mark spot taken, record ticket, unlock
L-->>G: Ticket T42
G-->>D: print ticket, raise barrier
D->>X: present T42
X->>L: unpark(T42)
L->>L: lock, close ticket, free spot, unlock
L->>P: fee(ticket, now)
P-->>L: 600 cents
L-->>X: amount due
X-->>D: pay, raise barrierDesign patterns used and why
- Strategy for allocation and pricing. These are the two policies the business changes most often: "keep large spots free for buses on weekdays", "first 15 minutes free", "flat evening rate after 18:00". Each becomes a new class behind one interface, and the lot never changes. The default allocator is smallest-fit: a motorcycle takes a small spot before it eats a medium one.
- Façade:
ParkingLotis the only thing gates talk to. It hides levels and spots, which lets you change the internal index (sorted lists today, a database tomorrow) without touching gate code. - Observer for display boards. Boards subscribe to "free count changed" events instead of polling. Keep the notification outside the lock, or a slow LED controller will stall your entry gates.
- State for the ticket lifecycle, kept light: an enum plus guarded transitions (active, paid, closed). A State-pattern class per state is overkill here.
- Dependency injection for the clock, in place of a global. Many textbook answers make
ParkingLota Singleton. Say why you would not: a singleton is a global, it makes tests share state, and a company that runs two garages needs two instances. "One per process" is a wiring decision formain, not a property of the class.
Concurrency and edge cases
- Last spot, two gates. Gate A and gate B both see one free medium spot. If "find" and "mark taken" are separate steps, both issue a ticket for it. The fix is to make selection and claim atomic: one mutex in memory, or one conditional update in the database (
UPDATE spot SET ticket = $1 WHERE id = $2 AND ticket IS NULL, checking the row count). - Same plate twice. A misread or a tailgater produces a second park for a plate already inside. Keep a set of active plates and reject the duplicate, then let an attendant resolve it.
- Double exit. A ticket scanned twice must not free the spot twice or charge twice. Remove the ticket from the active map under the lock; the second call finds nothing and fails cleanly.
- Time. Record entry time from the server's clock, never the kiosk's. Store UTC epoch seconds so daylight-saving changes cannot produce a negative or doubled hour. Compute durations from the same clock source. Do date arithmetic with a calendar library, never by bumping fields: "same date next year" does not exist after 29 February (see In the wild).
- Rounding. Decide and write down whether 61 minutes is one hour or two. The practice code uses ceiling hours after a 15-minute grace period, and does all arithmetic in integer cents. "Cents" really means the currency's minor unit: yen has no minor unit, so a payment API expects
500for 500 JPY but1000for 10 USD (Stripe docs). Store the amount with its currency code. - Lost ticket. Look the vehicle up by plate and charge a published lost-ticket maximum. This is why the plate index exists beyond duplicate detection.
- Payment fails after the barrier opens. Free the spot only when the ticket closes, and close it only after payment succeeds or an attendant overrides.
- Payment times out. The terminal does not know whether the charge went through. Retry with an idempotency key derived from the ticket id, so the processor returns the first result instead of charging twice. Stripe, for example, stores the first response per key and returns it on retries, and prunes keys after 24 hours (Stripe docs).
In the wild
- Microsoft Azure, 2012: the leap-day certificate. An agent inside each VM computed a certificate's expiry by adding one to the current year. At 00:00 UTC on 29 February that produced 29 February 2013, a date that does not exist, so certificate creation failed. After three failed attempts with 25-minute timeouts, hosts marked healthy servers as faulty, and the disruption lasted about 34 hours. Microsoft added testing and code analysis for time-related bugs and gave affected customers a 33% credit (Azure blog).
- Cloudflare, 2017: time went backwards. During the leap second at midnight UTC on 1 January, Go's wall clock stepped back, so Cloudflare's DNS server computed a negative round-trip time and passed it to
rand.Int63n, which panics on negative input. About 0.2% of DNS queries were affected at peak. The fix kept negative values out of server selection and reached every server by 06:45 UTC (Cloudflare). Go 1.9 later made durations betweentime.Now()values use a monotonic reading (Go 1.9 notes). A parking fee computed asexit - entryfrom the wall clock has the same bug.
Down the stack
Layer 1The contract
park either returns a ticket bound to exactly one spot that no other active ticket holds, or returns "full" and changes nothing. unpark on an active ticket frees that spot and returns fee(size, exit - entry); on an unknown or closed ticket it fails and changes nothing. freeCount is exact at the moment the lock is held and may be stale by the time a display shows it, which is acceptable because the gate re-checks.
Layer 2The mechanism
In memory, keep a free list per spot size, sorted by spot id so "lowest level first" falls out of the ordering, plus a hash map from ticket id to ticket and a set of plates. Park walks sizes from the vehicle's own upwards and pops the first free id, which is O(number of sizes) plus the cost of the list operation. A min-heap per size makes it O(log n). In a real garage this state lives in a database: one row per spot with a nullable ticket_id and an index on (size, ticket_id, level). A Postgres claim looks like SELECT id FROM spot WHERE size >= $1 AND ticket_id IS NULL ORDER BY size, id LIMIT 1 FOR UPDATE SKIP LOCKED, followed by the update, in one transaction. SKIP LOCKED lets two gates claim two different spots in parallel instead of queueing on the same row. The cost is a deliberately inconsistent view: rows another transaction holds are simply left out, which Postgres says suits queue-like tables, not general queries (Postgres docs). So a gate can see "full" while another gate's claim on the last spot is about to roll back, and the right reaction is a quick retry, not a "lot full" sign.
Layer 3The machine
An uncontended sync.Mutex in Go costs around 20 ns; a contended one parks the goroutine through a futex syscall, a few microseconds. A Postgres commit waits for the write-ahead log fsync, around 0.1 to 1 ms on an NVMe SSD. Compare the physical world: a loop detector needs tens of milliseconds to register a car, a barrier arm 2 to 6 seconds to rise, and 10 gates see at most a few arrivals per second. The software is three to four orders of magnitude faster than the hardware it serves, so a single global lock is not a bottleneck. On clocks: the wall clock can jump when NTP corrects it, so production code measures durations with a monotonic clock where it can and stores wall-clock timestamps only for receipts. Go builds this in: time.Now() carries both readings, and Sub uses the monotonic one when both values have it (Go time docs). A monotonic reading does not survive a process restart, though, so a ticket stored in a database still needs a wall-clock entry time and a clamp at zero.
Why it's built this way
- Spots are physical and scarce, so double allocation is a customer-facing failure: two cars, one space, an angry attendant.
- Preventing it needs one serialization point per spot, so selection and claim happen under one lock or one row lock.
- Gates arrive a few times per second at most, so a coarse lock is cheap and you do not need lock-free cleverness.
- Fees are disputed, so the fee must be recomputable from the ticket alone, which forces an injected clock and integer money.
- Policies change monthly while spots change almost never, so policies sit behind interfaces and the spot index stays simple.
Extensibility
- New vehicle type (EV): add a size or a capability flag on spots (
hasCharger) and an allocation strategy that prefers charger spots for EVs and avoids them for everyone else. The lot does not change. - New pricing (weekend flat rate, dynamic surge): a new
PricingStrategy. If prices change while cars are inside, snapshot the rate card id on the ticket at entry so the fee does not change under the customer. - Reservations: add a
HELDspot state with an expiry, the same pattern as seat holds in ticketing. The allocator treats expired holds as free, so correctness does not depend on a cleanup job running on time. - Multiple garages: the lot becomes one instance per site behind a service. State moves to a database partitioned by site id; each site's claims stay single-partition.
- Licence-plate recognition: a new entry gate implementation that calls
parkwith the recognised plate; the duplicate-plate check becomes more important, not less.
Variant: parcel locker station
A parcel locker is the same allocator with three changes: the occupant is a package instead of a driver, the exit is authorised by a secret instead of a ticket, and a deadline ends the stay. Compartments come in three sizes and the smallest-fit rule carries over unchanged, so ParkingLot becomes LockerStation with deposit, pickup(code) and collectExpired. The domain also gains a hardware boundary, so a DoorController interface (open, isClosed) sits beside the clock, with a fake for tests and one adapter per lock-board vendor.
The ticket becomes a bearer credential. A parking ticket is useless to a stranger because it is tied to a plate and a fee. A pickup code opens a door for whoever types it, so it needs properties the ticket never had. Draw it from a cryptographic source (Python's secrets, Go's crypto/rand, not random), redraw on collision with any active code, and store only a salted hash. Six digits give a million values, so a station holding 100 parcels has a 1 in 10,000 chance that a fresh code collides, and a stranger's random guess hits some live code about 100 times in a million. Count consecutive wrong attempts and lock the keypad for a few minutes after, say, five (OWASP guidance). Because the attacker must stand at one visible screen, this is enough here, where an online login needs much more.
The stay has a deadline. Parking is open-ended and billed at exit. A locker stay is capped, three days for Amazon's lockers, and an uncollected parcel blocks a scarce door. Each assignment stores a deadline, an expired code stops opening the door even though the parcel is still inside, and a sweep returns each expired package exactly once to the courier. Keep a min-heap by deadline so the sweep touches only what is due.
Compartment state needs more than free or taken. Use AVAILABLE, RESERVED, OCCUPIED and OUT_OF_SERVICE. The code is issued only after the door reports closed; until then the box may be empty or the courier may cancel. A door that never confirms closing sends the compartment to inspection and the courier to another box.
Offline is normal. A station in a petrol station loses its network, so it validates pickups from locally stored hashes, writes state to flash with fsync on every change, and syncs events later. The station, not the cloud, is the source of truth for which door is occupied.
Leadership lens
- Build first: the spot index with the atomic claim and a test that hammers the last spot from many threads. Pricing rules can be wrong for a week and refunded; double allocation cannot be undone.
- Ask in review: where is the single serialization point; what clock does the fee use; what happens if payment fails with the barrier already up; how is a rate change applied to cars already inside.
- Money is integer cents everywhere, and every fee is logged with its inputs (ticket id, entry, exit, rate card version) so support can answer disputes without engineers.
- Ownership: gate hardware integration and the allocation core are different skills. Give the gate team an interface (
park,unpark) and a simulator, so they are not blocked on real barriers to test. - In an interview, spend five minutes on requirements and fifteen on concurrency and change, not forty on a
Vehiclehierarchy.
Check yourself
Why is an enum for vehicle size better than
Car,BusandMotorcyclesubclasses in this design?Show answer
The only behaviour that varies is size, and an enum captures that with no dispatch or casting. Subclasses earn their place when types gain behaviour of their own, such as an EV needing a charger.
Two gates call
parkfor the last free spot at the same time. What exactly must be atomic?Show answer
Choosing the spot and marking it taken (plus recording the ticket). If those are separate steps, both callers can choose the same spot before either marks it.
Why should the pricing code not call the system clock itself?
Show answer
Then fees cannot be tested deterministically, replayed for a dispute, or computed consistently with the entry time. Injecting a clock makes the fee a pure function of recorded facts.
A car parks in a large spot because no medium spot is free. Which rate does it pay, and where does that rule live?
Show answer
That is a policy decision; the usual choice is the vehicle's rate, not the spot's. It lives in the pricing strategy, keyed by the size stored on the ticket.
Sources and further study
Primary
- Go 1.9 release notes: transparent monotonic time: the language-level fix for durations that go negative when the wall clock steps.
Systems docs
- OWASP Authentication Cheat Sheet: lockout and throttling guidance behind the locker variant's pickup-code attempt limit.
- Python secrets module: why security tokens such as pickup codes come from a cryptographic source rather than
random. - PostgreSQL: SELECT locking clause:
FOR UPDATE,NOWAITandSKIP LOCKED, and when each is appropriate. - Stripe: idempotent requests: how a payment API makes a timed-out charge safe to retry.
- Stripe: currencies and minor units: why "integer cents" must be "integer minor units plus a currency code".
Books
- Design Patterns: Elements of Reusable Object-Oriented Software, Gamma, Helm, Johnson and Vlissides: chapter 3 (Creational, including Singleton), chapter 4 (Structural, including Facade) and chapter 5 (Behavioral, including Strategy, Observer and State).
- Head First Design Patterns, Freeman and Robson: chapter 1 (Strategy), chapter 2 (Observer) and chapter 5 (Singleton, including its multithreading problems).
- Cracking the Coding Interview, Gayle Laakmann McDowell, chapter 7 "Object-Oriented Design": the parking lot problem in its original, hierarchy-first form.
Other curricula
- donnemartin/system-design-primer: parking lot: models vehicles as subclasses and buses as five consecutive large spots; it has no concurrency, pricing or clock.
- Refactoring.Guru: Singleton: presents Singleton as a standard pattern while listing its costs (harder unit testing, hidden coupling, thread-safety care), a useful contrast with this lesson's advice to inject one instance.
Watch
- Parking Lot Design | Grokking The Object Oriented Design Interview Question (Think Software): a classic walkthrough of the entities and use cases.
- Design A Parking Garage | Google SWE Teaches Low Level Design Episode 4 (Jordan has no life): a code-first take that spends time on concurrency.
- Strategy Pattern, Design Patterns ep 1 (Christopher Okhravi): a careful explanation of the pattern behind pluggable allocation and pricing.
Free resources
- Strategy pattern (Refactoring.Guru): the pattern used for allocation and pricing, with examples in many languages.
- Observer pattern (Refactoring.Guru): how display boards can subscribe to free-count changes.
- Explicit locking (PostgreSQL docs): row locks and
FOR UPDATE, the database version of the atomic claim. - Value Object (Martin Fowler): why vehicles and tickets are better as immutable values than as rich entities.
- Optimistic concurrency control (Wikipedia): the alternative to locking when claims rarely collide.