System design interview guide
Parking Lot System Design
Saturday mall rush: entry gates disagree with payment kiosks on whether level 3 is full because spot counts updated eventually. The OOD parking lot interview still needs concurrency on spots, ticket state, and payment—at scale add multi-entry coordination.
Problem statement
Parking lot: spot assignment, ticketing, payment, concurrency.
Introduction
Two cars pull into the same garage at the same moment. Both entry screens flash “Spot 12 — proceed.” One driver parks. The other finds a sedan already in the space. At the exit lane, a gate times out, retries payment, and the card gets charged twice.
That is what users feel when the backend treats parking like a class diagram instead of a state machine with rules.
Interviewers are not grading UML. They want to hear: what must never happen (two cars, one spot; two charges, one exit), and how your design survives retries, offline gates, and humans overriding the system.
Weak answers start with ParkingLot, Floor, Spot classes. Strong answers start with invariants—then draw boxes that enforce them.
If you remember one thing: State your invariants before you draw boxes. Spot assignment must be exact; availability counts can be approximate.
How to approach
Walk through the room in this order—not “here are my classes.”
-
Lock down the rules (about five minutes).
- Vehicle types: motorcycle, car, truck.
- Spot types: compact, regular, large (plus optional accessible/EV).
- Compatibility: decide it explicitly.
- Allocation policy: “nearest” (to which entrance?), “any on lowest floor”, or “zone-based”.
- Payment: on exit, on entry, or mixed? Any grace period?
- Are reservations allowed? Are attendants allowed to override?
-
Define the invariants (thirty seconds, but say them out loud).
- A spot has at most one active occupancy.
- A ticket/session has a single lifecycle (OPEN → PAID → CLOSED).
- Payment is idempotent and reconciled.
-
Walk one entry + one exit end-to-end.
- Entry: allocate spot atomically, create session, return ticket.
- Exit: compute fee, pay idempotently, release spot once.
-
Then talk about performance and “real-time” availability.
- Fast reads with cached counts.
- Correct allocation with a transactional source of truth.
In the room: “I’ll lock rules and invariants first, then walk entry and exit before I talk about caching availability.”
If you remember one thing: Rules → invariants → one entry story → one exit story—then performance.
Interview tips
Five common exchanges. Each shows a trap answer, a follow-up, and where to land.
“Nearest available spot”
You: “We pick the nearest free spot.”
They ask: “Nearest to what—the main entrance, the elevator, or the driver’s declared zone?”
Land here: Define nearest as a policy: same zone first, then lowest floor, then lowest spot number—or walking distance from a fixed entrance. Allocation still uses atomic claim on one spot row; “nearest” only picks the candidate set.
Source of truth
You: “We keep availability in Redis for speed.”
They ask: “Two entry lanes both see ‘Spot 12 free’—who wins?”
Land here: Spot occupancy lives in the database (transaction or unique constraint). Cached counts are for displays only—they can lag briefly but must reconcile from truth.
Concurrency and locks
You: “We use a lock.”
They ask: “What exactly is locked—a spot, a floor pool, or the whole lot?”
Land here: Lock the chosen spot row (SELECT … FOR UPDATE SKIP LOCKED) or enforce one OPEN session per spot via unique index. Pair with idempotency keys on entry so gate retries do not create two tickets.
Pricing and time
You: “Fee equals hours times rate.”
They ask: “Grace period? Rounding per minute or per hour? Lost ticket?”
Land here: State rounding rules, grace minutes, and max-day rate for lost tickets. Use server UTC time for session timestamps—not the gate device clock.
Overrides and messy reality
You: “Sensors always tell the truth.”
They ask: “A spot shows available but a car is physically there—now what?”
Land here: Attendants get an audited override API (OUT_OF_SERVICE, force release). Treat sensor events as at-least-once; reconcile divergent state instead of pretending hardware is perfect.
If you remember one thing: After each push, name one mechanism—row lock, idempotency key, unique constraint, or reconciliation job—not “we’ll handle it.”
Capacity estimation
Given the prompt numbers:
| Input | Estimate | Why it matters |
|---|---|---|
| Vehicles per day | ~2000 | Writes are moderate; correctness beats throughput. |
| Peak arrival rate | ~5/min | Concurrency exists, but not “internet scale.” Still enough to double-assign without atomicity. |
| Spots | ~1000 | A single database can handle it; the design focus is modeling + invariants. |
| Availability reads | High | Displays + apps read often; caching counts matters more than optimizing entry QPS. |
Implication: Your backend can be simple and still impress—if the invariants and lifecycle are crisp. You do not need internet-scale sharding; you do need atomic allocation under ~5 arrivals per minute.
If you remember one thing: Correctness beats throughput here—one double-assigned spot ruins trust faster than a slow display.
High-level architecture
Gates and sensors produce events. Your backend enforces legal state transitions.
[ Gate / Sensor ] → [ Entry/Exit API ] → [ Parking service ] → [ DB (truth) ]
| |
| → [ Cache / counters (fast reads) ]
|
→ [ Payments provider ] → [ Payment records ]
Key split: allocation happens against the DB in a transaction; availability can be served from counters that are eventually consistent.
In the room: Draw the split—DB owns “who is in spot 12”; cache owns “how many regular spots left on floor 2.”
If you remember one thing: Assign against truth; display from cache with reconciliation.
Data model (entities + invariants)
You don’t need a giant ERD—just enough to encode correctness.
Core entities
- ParkingLot: metadata (name, address), configuration (policies, pricing rules).
- Floor: id, level number, zones.
- Spot:
spot_id,floor_id,type(compact/regular/large),status(AVAILABLE/OCCUPIED/OUT_OF_SERVICE), optionalfeatures(EV, accessible). - Vehicle:
vehicle_id(or plate),type. - ParkingSession (ticket):
session_id,vehicle_id,spot_id,entry_time,exit_time?,status(OPEN/PAID/CLOSED),created_by(gate vs attendant),version. - Payment:
payment_id,session_id, amount, currency,status, provider refs,idempotency_key.
Invariants to enforce
- One active session per spot.
- One active session per vehicle (optional, but usually intended).
- Session lifecycle is monotonic: OPEN → PAID → CLOSED (no skipping backwards).
- Payment idempotency: the same “pay” request cannot double-charge.
The rest of the “OO design” flows naturally once these are clear.
If you remember one thing: Four invariants—one spot, one vehicle session, monotonic lifecycle, idempotent pay—drive every table and API.
Detailed design
Compatibility rules (make them explicit)
One simple policy:
- Motorcycle → any spot (compact/regular/large)
- Car → regular or large
- Truck → large only
Then layer policy rules:
- Accessible spots require
driverHasPermit=true - EV spots require
vehicle.isEV=true(or allow non-EV at high occupancy with an explicit policy)
The key is separating physical fit from policy eligibility.
Entry flow (allocate spot)
Goal: allocate one compatible spot atomically and return a ticket.
- Request: vehicle type + optional preferences (floor, zone, accessible, EV).
- Parking service selects candidate set (by policy).
- Atomically claim one spot and create a session.
- Return
{ session_id, spot_id, floor, instructions }.
Atomic claim can be done in multiple ways; pick one and defend it.
Option A: transaction on spot row (simple and strong).
- Query candidate spot ids (bounded).
SELECT spot WHERE status=AVAILABLE ... FOR UPDATE SKIP LOCKED(or equivalent).- Update spot to OCCUPIED.
- Insert ParkingSession with status OPEN.
Option B: unique constraint on active occupancy.
- Insert session row with
(spot_id, status=OPEN)where a partial unique index enforces only one OPEN session per spot. - If conflict, retry with another spot.
For this scale, Option A is easy to reason about.
Exit + payment flow
Goal: compute fee, charge once, release spot once.
- Exit request arrives with
session_id(ticket) or plate lookup (lost ticket flow). - Compute fee from entry_time → now (apply grace/rounding).
- Create/confirm a Payment record using an idempotency key:
- Key could be
pay:{session_id}:{amount}:{pricing_version}.
- Key could be
- If payment succeeds, transition session to PAID, then CLOSED.
- Release spot (OCCUPIED → AVAILABLE) exactly once.
Important: don’t release the spot before confirming payment unless you explicitly allow “pay later” with an attendant override.
In the room: Narrate entry as one transaction: claim spot → insert OPEN session → return ticket. Exit: fee → idempotent pay → PAID → CLOSED → release spot once.
If you remember one thing: Entry is claim + session; exit is pay once + release once.
API design
Keep the surface small and state-driven.
POST /v1/entry→ allocate a spot, create sessionPOST /v1/exit→ compute fee, start payment (or return amount + payment token)POST /v1/payments/confirm→ confirm payment for a session (idempotent)GET /v1/availability?floorId=&spotType=→ counts + optional breakdownPOST /v1/spots/{spotId}/override→ attendant sets OUT_OF_SERVICE / forces AVAILABLE (audited)
API flow sketch:
Client/Gate → POST /entry
→ allocateSpot(txn) → createSession(OPEN) → return ticket
Client/Gate → POST /exit
→ computeFee → createPayment(idempotent) → confirm → closeSession → releaseSpot
If you remember one thing: Small, state-driven API—entry allocates, exit settles, pay confirms idempotently.
Concurrency: the “two cars, one spot” problem
This is where seniors separate from templates.
What can go wrong
- Two entry lanes both see “Spot 12 is free” and both assign it.
- Gate controller times out and retries
/entry, creating two sessions. - Exit is processed twice (double release) or payment is confirmed twice (double charge).
Techniques that actually work
- Row-level locking on the chosen spot (transaction).
- Optimistic concurrency with a
versionfield onSpot/Session. - Idempotency keys on entry and payment confirmation:
entry:{gate_id}:{request_id}pay:{session_id}:{amount}
- Uniqueness constraints:
- One OPEN session per
spot_id. - One OPEN session per
vehicle_id.
- One OPEN session per
A great answer says which combination you’ll use and why.
If you remember one thing: Combine row lock or unique constraint with idempotency keys—locks stop races; keys stop retries.
Failure handling (real world, not perfect world)
Devices offline or duplicated events
Treat gate/sensor events as at-least-once. Your backend must:
- Deduplicate by idempotency key.
- Allow reconciliation: if a sensor says “occupied” but DB says available, flag for attendant review.
“Lost ticket” / missing correlation
Make it explicit:
- If user lost the ticket, you can locate active session by plate.
- If you can’t prove entry time, charge a max-day rate and record a “manual override” reason.
Overrides (humans exist)
Add an audited override API:
- Spot OUT_OF_SERVICE.
- Force release with reason + actor identity.
- Create session manually.
The insight is that overrides are not “edge cases”; they are how systems survive reality.
If you remember one thing: Plan for duplicate events, lost tickets, and human overrides—they are normal ops, not rare bugs.
Production angles
Real garages do not match the whiteboard. Displays lie, gates retry, attendants override. These are the incidents that separate “I drew classes” from “I shipped parking software.”
Availability says 20 free, but the lot feels full
What users saw — Entry signs show open spots. Drivers circle and cannot find them. Attendants get complaints. Spot-by-spot reality disagrees with the big number on the display.
Why — Cached counts drift from truth. Sensors miss updates. OUT_OF_SERVICE spots still count as available. Overrides do not emit the right state transition.
What good teams do — Define availability from the spot state machine (AVAILABLE, OCCUPIED, RESERVED, OUT_OF_SERVICE). Rebuild counters from truth hourly or nightly. Alert when abs(derived_count - cached_count) > threshold.
Gate retries create duplicate sessions
What users saw — One vehicle gets two tickets. Billing disputes follow. Occupancy looks inflated—two sessions, one car.
Why — Network timeouts cause the gate to retry /entry. The backend treats each retry as a new allocation.
What good teams do — Require Idempotency-Key on /entry. Store request log keyed by idempotency key → session_id. Make entry safe to retry even when the client never got the first response.
Double-charging on payment confirmation
What users saw — Card charged twice for one stay. Support volume spikes. Finance reconciles manually.
Why — Payment providers encourage retrying confirmations. Your system treats each “confirm” as a new charge.
What good teams do — Create a PaymentIntent and confirm idempotently. Store provider reference id with uniqueness. Release the spot only after payment reaches a terminal success state—or document an explicit pay-later override path.
Clock skew breaks pricing
What users saw — Negative durations on receipts. Weird fees around daylight saving time. Disputes at exit kiosks.
Why — Entry time came from a device clock. Time zones were ambiguous.
What good teams do — Use server time (UTC) for all session timestamps. Treat device time as advisory metadata only.
How to use this in an interview — Pick one story: “counts diverge” or “duplicate tickets.” Name one symptom, one root cause (cache without reconciliation, or missing idempotency), and one fix (state machine + idempotency keys + constraints). That proves operational thinking.
What should stick
You do not need to memorize every entity. After this guide, you should be able to:
- Invariants first — One active session per spot; idempotent pay; monotonic lifecycle—before any class diagram.
- Truth vs display — DB owns occupancy; cache owns fast counts with reconciliation.
- Atomic claim — Row lock or unique constraint stops two cars taking one spot.
- Idempotent gates — Retries on entry and payment must not duplicate tickets or charges.
- Messy world is normal — Sensors lie, attendants override, tickets get lost—design for reconciliation.
Tell it in the room: “I define vehicle/spot rules and invariants first. Entry: atomically claim one compatible spot and create an OPEN session with an idempotency key. Exit: compute fee, pay once idempotently, then release the spot once. Availability displays use cached counts rebuilt from the spot state machine—not the other way around.”
Reference diagram

What interviewers expect
Spot lock; ticket state machine; payment idempotency; clear class diagram.
Core challenges
- Atomic spot assignment under concurrency
- Multiple entry lanes same lot
- Pricing by hour/dynamic
- Lost ticket flow
- EV/special spots reserved
- Display board eventual consistency
Interview workflow (template)
- Clarify requirements. Confirm functional scope, users, consistency needs, and which non-functional goals matter most (latency, availability, cost).
- Rough capacity. Estimate QPS, storage, and bandwidth so your data model and partitioning story are grounded.
- APIs and core flows. Define a minimal API and walk 1–2 critical read/write paths end to end.
- Data model and storage. Choose stores for each access pattern; call out hot keys, indexes, and retention.
- Scale and failure. Add caching, sharding, replication, queues, or fan-out as needed; say what breaks in failure modes.
- Tradeoffs. Name alternatives you rejected and why (e.g. strong vs eventual consistency, sync vs async).
Frequently asked follow-ups
- Assign spot?
- Concurrency?
- Multiple lots?
- Payment?
- Scale?
Deep-dive questions and strong answer outlines
Assign spot?
Transaction: find free spot row FOR UPDATE; mark occupied; issue ticket id.
Concurrency?
DB row locks or compare-and-set on spot status; retry if conflict.
Entry/exit?
Ticket links vehicle; payment at exit computes fee; gate opens on paid.
Full lot?
Count free spots cache with periodic reconcile; stop entry when zero.
Scale many lots?
Shard by lot_id; independent services per garage.
AI feedback on your design
After a practice session, InterviewCrafted summarizes strengths, gaps, and interviewer-style expectations—similar to a written debrief. See a static example report, then practice this problem to get feedback on your own answer.
FAQs
Q: OOD only?
A: Start OOD; mention DB locks if asked scale.
Q: Reservations?
A: Hold spot with TTL before arrival.
Q: Sensors?
A: IoT updates spot status—eventual sync.
Q: Valet?
A: Separate queue state machine.
Practice interactively
Open the practice session to use the canvas and stages, then review AI feedback.