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. Two cars can both be told to park in the same space unless assignment is atomic. The interview still needs concurrency on spots, a clear ticket lifecycle, and idempotent payment—at scale you add multi-entry coordination and reconciliation between displays and truth.

Problem statement
Design a parking lot system that assigns compatible spots on entry, tracks each stay as a ticket with a clear lifecycle, charges on exit without double billing, and shows availability that is fast for drivers but honest enough that two cars never park in the same space.
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 when sensors lie.
Weak answers start with ParkingLot, Floor, and Spot classes and stop. Strong answers start with invariants: at most one active occupancy per spot; one ticket lifecycle; payment that survives duplicate confirms. Then they draw boxes that enforce those rules.
Parking feels “low level” because the entities are familiar. The system-design depth is the same pattern as booking the last vaccine slot or selling the last concert seat: many readers, one writer, one winner. The difference is pace—five arrivals per minute at a mall garage, not a million refreshes per second—but the concurrency mistake is identical: read “free,” decide later, write, and hope.
If you remember one thing: State your invariants before you draw boxes. Spot assignment must be exact; availability counts can be approximate for displays.
How to approach
Start the way a driver experiences the garage—not by listing databases. Spend the first few minutes turning a vague prompt into a contract. Talk through clarifying questions out loud, then walk one entry and one exit end to end before you argue about caching.
Clarifying questions (say these in the room)
You: “Which vehicle types must we support—motorcycle, car, truck—and which spot types exist: compact, regular, large, plus optional EV or accessible?”
Interviewer: “Motorcycle, car, truck; spots are compact, regular, large; a few EV and accessible spots.”
That answer forces you to separate physical fit (car cannot use compact) from policy (EV spot reserved for EV unless you declare overflow rules).
You: “When we assign ‘nearest available,’ nearest to what—the main entrance, a zone the driver selects, or lowest floor first?”
Interviewer: “Define a policy; consistency matters more than the exact heuristic.”
Now you can name a rule—same zone, then lowest floor, then lowest spot number—and say allocation still atomically claims one row; “nearest” only narrows the candidate set.
You: “Is payment on exit only, or prepay on entry? Any grace period before hourly billing starts?”
Interviewer: “Exit payment with a short grace period is fine.”
That locks the exit flow: compute fee at leave, idempotent pay, then release spot.
You: “Do we need reservations, or walk-in only? Can attendants override assignment or mark spots out of service?”
Interviewer: “Walk-in is required; reservations optional. Attendants can override.”
Overrides become first-class APIs with audit—not a footnote.
You: “I want to leave fleet-wide navigation routing, a full payment processor implementation, and license-plate OCR accuracy out of scope so we can focus on spot contention, tickets, and payment idempotency. Does that match what you want?”
Interviewer: “Yes—focus on assignment and entry/exit.”
Saying out of scope early keeps the round on the hard part.
Minute pacing and order in the room
| Phase | Time | What to cover |
|---|---|---|
| Rules and invariants | ~5 min | Vehicle/spot types, allocation policy, payment timing, reservations |
| One entry story | ~8 min | Atomic claim, OPEN session, idempotency key, ticket returned |
| One exit story | ~8 min | Fee rules, idempotent pay, PAID → CLOSED, release spot once |
| Availability and displays | ~5 min | Cached counts vs DB truth, reconciliation |
| Scale and failures | ~remaining | Multi-lot sharding, sensor drift, lost ticket |
- Clarify scope (types, policy, payment, reservations, overrides).
- State invariants out loud—one spot, one lifecycle, idempotent pay.
- Walk entry: claim spot in one transaction, create OPEN session.
- Walk exit: fee, pay once, release spot once.
- Explain how display boards can lag briefly but assignment never lies.
- Name failure modes: gate retry, lost ticket, sensor mismatch.
In the room (a fuller opening you can actually say): “I will lock vehicle and spot rules first, then state invariants: one active session per spot, monotonic ticket lifecycle, idempotent payment. Entry atomically claims one compatible spot with a row lock and returns a ticket; gate retries use an Idempotency-Key so we do not issue two sessions. Exit computes fee from server UTC time, charges once, then releases the spot exactly once. Availability signs read cached counts rebuilt from spot state—the database owns who is in spot 12.”
Out of scope (and why you park these)
Turn-by-turn navigation inside the garage. Finding spot 214 on a map is a product feature; the interview depth is claiming spot 214 exactly once.
Building the card network or PCI vault. You need idempotent payment confirmation; you do not need to design Stripe in the round.
Perfect real-time computer vision on every bay. Plate recognition may exist at the gate; the design assumes you get a vehicle id or plate string and focus on session correlation and lost-ticket policy.
City-wide traffic routing between lots. Multi-lot search can be a read aggregator; assignment writes stay per lot_id.
Capacity estimation
Before you draw architecture boxes, use rough numbers so you do not promise internet-scale sharding for a single mall garage—or under-design concurrency because “it is only parking.”
| Input | Estimate | Why it matters |
|---|---|---|
| Vehicles per day | ~2,000 | Moderate write rate; correctness beats raw throughput. |
| Peak arrival rate | ~5/min across all entry lanes | Low compared to flash sales—but enough to double-assign without atomicity. |
| Spots per lot | ~1,000 | One primary database per lot is realistic; focus is modeling + invariants. |
| Availability reads | High (signs, apps, kiosks) | Reads dominate; cache counts, not claims. |
| Exit payment bursts | Evening peak ~10/min | Idempotent payment matters more than sub-millisecond fee math. |
| Multi-lot operator | 50–500 lots | Shard by lot_id; no single row for “all spots in the city.” |
Three practical lessons:
- Correctness beats throughput for assignment—a single double-booked spot destroys trust faster than a slow display board.
- Counters are cheap; claims are precious—optimize sign refresh and app polls, not the transactional entry path.
- Gate retry rate may be 1–3% on flaky networks—design entry and payment for duplicate requests from day one.
If you remember one thing: You do not need national-scale queueing; you do need atomic allocation when two lanes pick the same free spot.
High-level architecture
What breaks if you merge everything
One API that assigns a spot, charges a card, updates LED signs, emails a receipt, and writes analytics—all synchronously at the entry gate—means the driver waits on the slowest step. A payment timeout should not block the next car from entering.
The design also fails if each entry lane keeps its own list of free spots in memory. Lane A and Lane B can both believe Spot 12 is free. Caching “20 regular spots left” in Redis without a reconciliation path to spot rows produces angry drivers who cannot find an open bay.
What works: separate claim, display, and payment
Break responsibilities clearly:
Entry/Exit API — Gate-facing endpoints with idempotency keys; validates vehicle type and policy; orchestrates claim and release.
Parking service — Owns spot state machine, session lifecycle, compatibility rules, and fee calculation policy.
Authoritative database — Source of truth for spot occupancy and open sessions; allocation runs here in a transaction.
Availability cache — Derived counters per floor and spot type for fast signs; rebuilt from spot rows; may lag seconds.
Payment service — Creates payment intents, confirms with provider, stores idempotent payment records; does not own spot rows.
Attendant / admin API — Audited overrides (OUT_OF_SERVICE, force release, manual session).
[ Entry/Exit Gate ] --> [ Entry/Exit API ] --> [ Parking service ] --> [ DB (truth) ]
| | |
| | +--> [ Cache: free counts ]
| |
+--> [ Sensor events ] +--> [ Payment provider ] --> [ Payment records ]
(async confirm OK)
In the room: Draw the split—database owns “who is in spot 12”; cache owns “how many regular spots left on floor 2.” Entry always claims on the database; signs may read cache.
If you remember one thing: Assign against truth; display from cache with reconciliation.
Core design approaches
Interviewers want comparisons, not one diagram and silence.
Spot selection policies
| Policy | Plain idea | Good when | Watch out |
|---|---|---|---|
| Nearest to entrance | Minimize walking from a fixed gate | Single main entrance, simple UX | “Nearest” undefined → ask which entrance |
| Lowest floor first | Fill lower levels before upper | Structures where upper levels are premium | May cluster traffic on one floor |
| Zone-based | Driver picks zone A/B/C; assign inside zone | Large airports or stadiums | Empty zones while others full—need overflow rule |
| Type pool only | “Any regular spot” without naming id until ticket | Fast gates, simple signs | Driver needs clear directions after claim |
Whatever policy you pick, selection chooses candidates; one transaction claims exactly one row. Policy does not replace locking.
Single lot vs multi-lot
| Model | Plain idea | Good when | Watch out |
|---|---|---|---|
| Single lot service | One database partition per garage | Mall, airport terminal | Hot rows only within that lot |
| Multi-lot operator | Many lots, shared billing/reporting | City operator, chain garages | Do not centralize all spot rows on one shard |
| Federated search | Read aggregator across lots | App shows “parking near me” | Entry/exit writes still local to owning lot |
Interview land: “Shard inventory by lot_id. Cross-lot browse is a read path; claim is always on the lot that owns the spot.”
Reservation vs walk-in
| Model | Plain idea | Good when | Watch out |
|---|---|---|---|
| Walk-in only | Claim AVAILABLE spot at entry | Simple interview scope | Evening rush races on last spots |
| Hold then arrive | RESERVED with TTL until check-in | EV or accessible spots scarce | Expired holds need cleanup job |
| Mixed pools | Reserved pool + walk-in pool | Operators need both | Policy for stealing reserved spots at high occupancy |
For walk-in contention, use the same atomic claim as vaccine last-slot booking: one transaction, one winner. For reservations, transition RESERVED → OCCUPIED on check-in with the same lock pattern.
If you remember one thing: Policy picks candidates; the database pick one winner.
Data model
You do not need a giant ERD—just enough tables to enforce invariants.
Core entities
| Entity | Key fields | Role |
|---|---|---|
ParkingLot | lot_id, name, address, pricing config | Shard boundary |
Floor | floor_id, lot_id, level | Groups spots |
Spot | spot_id, floor_id, type, status, features | compact/regular/large; AVAILABLE/OCCUPIED/RESERVED/OUT_OF_SERVICE; EV, accessible |
Vehicle | vehicle_id, plate, type | motorcycle/car/truck |
ParkingSession | session_id, vehicle_id, spot_id, entry_time, exit_time, status, version | Ticket: OPEN → PAID → CLOSED |
Payment | payment_id, session_id, amount, status, idempotency_key, provider refs | Idempotent settlement |
EntryRequestLog (optional) | idempotency_key, session_id, gate_id | Dedup gate retries |
Compatibility (physical fit)
- Motorcycle → compact, regular, or large
- Car → regular or large
- Truck → large only
Layer policy on top: accessible spots require permit flag; EV spots require vehicle.isEV unless overflow policy says otherwise.
Invariants to enforce
- At most one active session per spot — partial unique index on
(spot_id)wherestatus = OPEN(or OPEN/PAID before CLOSED). - At most one open session per vehicle (usual product rule) — unique on
(vehicle_id)where active. - Monotonic session lifecycle — OPEN → PAID → CLOSED; no backward transitions without audited override.
- Payment idempotency — same
idempotency_keyreturns the same payment outcome; provider reference unique.
The object-oriented class diagram should mirror these rules—not replace them.
If you remember one thing: Four invariants drive every table and API: one spot, one vehicle session, monotonic lifecycle, idempotent pay.
Detailed design
This section walks the same two journeys you would draw on the whiteboard: a car entering and getting a ticket, then later paying and leaving. The goal is not a checklist of SQL verbs alone—it is to make clear who decides, what becomes durable, and what the gate displays at each step.
Entry flow (allocate spot)
When a vehicle reaches the entry lane, the driver needs one thing that feels instant: a ticket (or on-screen instructions) that names a real parking spot. Behind that moment the system must do three careful jobs. First, it must recognize the vehicle type and any preferences the product allows—floor, zone, accessible, EV. Second, it must pick a spot that is physically large enough and allowed by policy. Third, it must claim that spot for this vehicle alone so a second lane cannot be told the same spot is free.
The gate or app sends that request with an idempotency key—for example entry:{gate_id}:{request_id}. That key matters because flaky networks and double-taps are normal at barriers. If the same key arrives again, the service returns the same session and spot that already succeeded. It must not invent a second open parking session for one physical entry.
Once the request is new, the parking service builds a short list of candidate spots: matching vehicle type, status available (or reserved for this reservation id), not marked out of service, ordered by the selection policy you agreed earlier (nearest aisle, fill one floor first, and so on). Then it enters one database transaction that both locks and updates:
Inside that transaction the service asks the database for one available row using a locking read (FOR UPDATE SKIP LOCKED or an equivalent). SKIP LOCKED means “if another lane already locked this row, skip it and try another”—so two gates do not block forever waiting on the same row. When a free spot is found, the service marks it occupied, inserts a parking session in the OPEN state with a server-side entry timestamp, and only then commits. The ticket returned to the gate includes session_id, spot_id, floor, and short driving instructions.
If every candidate is gone or locked by others, the service returns a clear “lot full” (or temporarily unavailable) response—never a made-up spot id. Availability screens may still show a stale free count; entry always tries a real claim and trusts the transaction, not the sign.
There is a second style that some teams prefer: insert the OPEN session and let a unique constraint “only one OPEN session per spot” reject a double insert, then retry the next candidate. Either story is fine in the interview as long as you show why two cars cannot both win without a lock or unique rule.
Exit and payment flow
Exit is a different state machine. The driver presents the ticket (session_id) or a plate lookup if the ticket is lost. The service loads that session. If the session is already paid and closed, it returns the previous outcome—because payment confirmation messages also retry, and charging twice for one visit destroys trust.
If the session is still open, the service computes the fee from server UTC time, not from the phone’s clock. That usually means: duration since entry, minus any grace minutes, rounded by the product rule (per minute or per started hour), and with a documented policy for lost tickets or daily caps. The pricing rules should be versioned so a mid-day rate change does not silently change how old sessions were charged.
Payment is submitted to the processor with its own idempotency key, such as pay:{session_id}:{amount}:{pricing_version}. Only after the provider confirms success should the session move to PAID and then CLOSED, and only then should the spot become AVAILABLE again. Opening the gate before money clears is a product choice (pay later / valet debt)—if you allow it, track the debt explicitly. Do not casually set the spot free and hope payment finishes later unless you are ready to explain that debt model.
Receipts and analytics can leave asynchronously after the critical path is safe. The user’s mental model is simple: ticket in, pay once, gate opens once, spot frees once.
Availability reads
Screens that say “47 free spots” are a display problem, not the claim path. They typically read cached counts or a materialized view refreshed every few seconds. That lag is acceptable for humans driving past a board. It is not acceptable for the entry API, which must always attempt a transactional claim. In the room, say out loud: “Signs can be slightly wrong; entry cannot assign a ghost spot.”
In the room (spoken): “On entry I claim one compatible spot in a single database transaction, create an open session with server time, and return a ticket. Retries reuse the same idempotency key so I never open two sessions for one gate request. On exit I compute the fee from server time, charge once with a payment idempotency key, then close the session and free the spot—never the reverse unless we explicitly support pay-later.”
Architectural dig: two cars, one spot
This is the interview meat—same tension as booking the last vaccine slot, with garage gates instead of browsers.
What goes wrong (bad design: read free, then write)
t0 Lane A reads Spot 12 status = AVAILABLE
t0 Lane B reads Spot 12 status = AVAILABLE
t1 A writes OCCUPIED + inserts session SA
t1 B writes OCCUPIED + inserts session SB
two tickets, one physical space
Or: gate times out after commit, retries /entry without idempotency—two sessions for one car. Or: exit confirm retries—double charge, or double release that frees a spot while a car still parks there.
What works (great design: row lock + one OPEN session)
BEGIN
SELECT spot_id FROM spots
WHERE lot_id = ? AND type compatible AND status = AVAILABLE
ORDER BY policy
LIMIT 1
FOR UPDATE SKIP LOCKED
IF no row → ROLLBACK → 409 lot full
UPDATE spots SET status = OCCUPIED
INSERT parking_sessions (spot_id, status=OPEN, entry_time=utc_now())
COMMIT → return ticket
Lane B’s transaction skips the row A locked and takes the next spot—or waits briefly and fails full.
Pair with:
- Idempotency-Key on entry — retry returns same
session_id. - Partial unique index — one OPEN session per
spot_id. - Idempotent payment confirm — same key returns same charge id.

Figure: Display cache may say “1 left”; claim on the primary decides the single winner.
Interview land: “Signs are a hint; entry is the lock—one short transaction, one winner per spot.”
Key challenges
| Challenge | What users see if it goes wrong | What to say in the interview |
|---|---|---|
| Double assignment | Two cars, one spot; fights and liability | Row lock or unique OPEN session; never read-then-write across requests |
| Gate retries | Duplicate tickets; inflated occupancy | Idempotency-Key on entry mapped to session_id |
| Stale availability | “20 free” but lot feels full | Counters from spot state machine; reconcile and alert on drift |
| Payment retry | Double charge at exit | Payment idempotency key + provider reference uniqueness |
| Policy vs fit | Truck in compact spot or ICE in EV bay | Separate physical compatibility from policy flags |
| Lost ticket | Long exit line, arbitrary fees | Plate lookup; max-day rate; audited manual override |
| Sensor drift | DB says empty; bay occupied | Reconciliation and attendant override—not blind sensor overwrite |
Correct spot assignment matters more than fancy pricing engines. Drivers forgive a slow sign; they do not forgive two tickets for one space or a double charge.
Scaling the system
Per-lot partition: Store spots and sessions under lot_id. A busy mall peaks independently; sharding prevents one garage from blocking another.
Read replicas for availability: Signs and apps poll counts from read-optimized stores or cache. Entry and exit writes stay on the primary for that lot so claims never use stale occupancy.
Hot spots within a lot: Popular floors at Saturday peak create contention on many rows—but SKIP LOCKED spreads lanes across different spot rows instead of serializing the whole lot on one counter.
Background workers: Receipt email, counter rebuild, reservation TTL expiry, sensor reconciliation—async after commit. Do not block gate open on SMTP.
Multi-region: Usually one lot lives in one region; cross-region assignment is rare. Operator dashboards aggregate asynchronously.
Watch 409 rate at entry (lot full) and claim latency p99 per lot. A spike in full responses with low occupancy in truth means cache drift or bad policy filters—not necessarily true capacity limits.
Failure handling
| Scenario | What the user sees | What to build |
|---|---|---|
| Database slow at entry | Gate spinner; queue backs up | Short transaction; timeout with safe retry via idempotency key; do not partial-commit spot without session |
| Payment provider down | Cannot exit automatically | Queue at exit with attendant override path; session stays OPEN; spot stays OCCUPIED |
| Duplicate sensor event | Flickering spot status | Dedup by (sensor_id, event_id); reconcile against session truth |
| Cache lost | Signs blank or wrong | Rebuild counters from spot table; entry still works from primary |
| Clock skew on gate device | Wrong fee | Server UTC for entry_time; device clock advisory only |
| Attendant force release | Spot marked free while car present | Audited override; optional sensor alert |
When assignment correctness is uncertain—database unhealthy, transaction aborts mid-claim—fail closed on new entry rather than guessing free spots. A closed gate frustrates drivers; assigning two cars to one bay creates a longer support and safety incident.
API design
Keep the surface small and state-driven.
| Endpoint | Role |
|---|---|
POST /v1/entry | Allocate spot, create OPEN session; requires Idempotency-Key |
POST /v1/exit | Compute fee, initiate or confirm payment |
POST /v1/payments/confirm | Idempotent payment confirmation for session |
GET /v1/availability | Counts by floor and spot type (may be stale) |
POST /v1/spots/{spotId}/override | Attendant OUT_OF_SERVICE / force AVAILABLE (audited) |
GET /v1/sessions?plate= | Lost-ticket lookup (rate-limited, audited) |
GET /v1/availability parameters
| Param | Role |
|---|---|
lotId | Which garage |
floorId | Optional filter |
spotType | compact / regular / large |
features | ev, accessible |
Request flow
Gate → POST /v1/entry (Idempotency-Key)
→ parking.allocateSpot(txn: SKIP LOCKED)
→ createSession(OPEN)
→ 201 { session_id, spot_id, floor }
Gate → POST /v1/exit
→ computeFee(session)
→ payments.confirm(idempotent_key)
→ session PAID → CLOSED; spot AVAILABLE
→ 200 { receipt }
Errors: 409 no compatible spot (lot full); 402 payment required; 404 unknown session; 429 gate rate limit. Duplicate Idempotency-Key returns same body as first success.
If you remember one thing: Entry allocates; exit settles; payment confirms idempotently—three verbs, one lifecycle.
Observability
Do not watch only “entry API 200 rate.” That misses double assignment and cache lies.
| Signal | Why it matters |
|---|---|
entry.claim latency p99 | Gate SLO; long transactions block lanes |
entry.conflict / 409 rate | True full vs stale policy vs drift |
spot.double_open_sessions | Should be zero—alert immediately |
cache.count_drift per floor | abs(cached − derived from spots) |
payment.duplicate_idempotency_hits | Healthy dedup vs bug |
exit.payment_failure_rate | Provider or network pain at evening peak |
override.count by attendant | Operational load and sensor distrust |
Alert ideas: any row with two OPEN sessions on one spot; drift above threshold for 10 minutes; payment confirm without matching OPEN session; spike in entry retries without idempotency headers.
If you remember one thing: Measure truth violations (double session, negative free count), not only HTTP success.
Security and abuse
Authenticate gate devices and attendant tools before entry and override APIs—anonymous callers should not mint tickets or free spots. Rate-limit entry and plate lookup to prevent scanning for open sessions. Idempotency keys must be unguessable and scoped per gate. Payment endpoints require the same auth plane as exit kiosks; store provider tokens encrypted. Attendant overrides need role-based access and immutable audit (who, when, reason). Do not log full card numbers or plates in searchable debug logs; correlate by session_id in redacted form. Stolen gate credentials need revoke and rotation without draining the whole lot inventory.
Cost awareness
Every double booking wastes attendant time, refunds, and trust. Every stale “20 free” sign wastes circling traffic and complaints—not CPU dollars, but operator cost. Caching availability is cheap; reconciling it nightly is cheaper than wrong-way drivers blocking aisles. Payment retries without idempotency create support tickets that cost more than the parking fee. Shard by lot so one viral mall does not force a city-wide database scale-up. Sensor ingestion and reconciliation have storage cost—sample and dedup events rather than writing every flicker to the primary path.
Production scenes
Textbooks teach boxes and arrows. On-call teaches this: the dashboard can look fine while drivers are furious.
Scene 1: Availability says 20 free, but the lot feels full
The moment: Saturday rush. LED signs show open spots. Drivers circle for twenty minutes. Attendants radio that level 3 is packed.
The trap: Cached counters drifted from truth. OUT_OF_SERVICE spots still counted as available. Overrides did not emit counter updates. Sensors lagged so OCCUPIED bays looked free on the board.
What to build: Derive counts only from spot status. Rebuild cache hourly and on state-change events. Alert when drift exceeds threshold. Entry still claims atomically—signs are hints, not contracts.
Scene 2: Gate retries create duplicate sessions
The moment: One vehicle has two printed tickets. Occupancy is wrong by one. Billing disputes at exit.
The trap: Network timeout after commit; gate retries POST /entry without Idempotency-Key. Backend creates a second OPEN session for the same vehicle.
What to build: Require Idempotency-Key on entry; persist key → session_id. Retries return the first ticket. Enforce one OPEN session per vehicle via unique index.
Scene 3: Double charge at payment confirmation
The moment: Card charged twice for one stay. Support queue spikes. Finance runs manual refunds.
The trap: Payment provider encourages confirm retries; each retry creates a new charge row.
What to build: PaymentIntent with idempotent confirm; store provider reference with uniqueness. Return same receipt on replay. Release spot only after terminal payment success.
Scene 4: Clock skew breaks pricing
The moment: Receipt shows negative duration. Fee jumps at daylight saving change. Driver argues at kiosk.
The trap: entry_time taken from gate device clock. Mixed time zones on receipts.
What to build: Server UTC for all session timestamps. Device time stored as metadata only. Unit tests around DST boundaries.
In the interview: Pick one scene. Tell what users felt, which metric lied, and one fix—state machine plus idempotency plus constraints—not “we would monitor it.”
Bottlenecks and tradeoffs
Exact occupancy vs fast displays
The tension: Drivers want instant sign updates; exact per-spot truth on every LED is expensive and still stale for seconds.
What breaks: Treating display cache as source of truth for assignment.
What teams do: Transactional claim on primary; approximate counters for boards; reconcile continuously.
Say in the interview: “Signs lag; entry never lies.”
Row lock contention vs lot full latency
The tension: FOR UPDATE SKIP LOCKED spreads lanes across rows; when the lot is truly full, every lane scans candidates and fails.
What breaks: Long entry latency at 99% occupancy if candidate queries are unindexed.
What teams do: Index (lot_id, type, status); short-circuit when derived count is zero; keep transactions tiny.
Say in the interview: “Index the candidate query; fail fast when policy says full.”
Pay on exit vs pay on entry
The tension: Exit payment matches actual stay duration; entry prepay speeds exit but refunds get messy.
What breaks: Releasing spot before pay on exit if confirm is slow—another car assigned while first still parked.
What teams do: Default pay-on-exit with idempotent confirm; optional prepay with explicit “spot held until paid” policy.
Say in the interview: “I release the spot only after payment succeeds unless attendant pay-later is explicit.”
If you remember one thing: The bottleneck is usually correctness under retry, not QPS.
Interview tips
You have the full design in mind now. These five back-and-forths are how interviewers test whether you can defend it under pressure.
“Nearest available spot”
You might say: “We pick the nearest free spot.”
Interview Push: “Nearest to what—the main entrance, the elevator, or the driver’s declared zone?”
Land here: Define nearest as a written 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 orders the candidate set.
Source of truth for availability
You might say: “We keep availability in Redis for speed.”
Interview Push: “Two entry lanes both see ‘Spot 12 free’—who wins?”
Land here: Spot occupancy lives in the database (transaction or unique constraint). Cached counts power displays—they can lag briefly but must reconcile from spot status. Signs do not assign; entry does.
Concurrency and locks
You might say: “We use a lock.”
Interview Push: “What exactly is locked—a spot, a floor pool, or the whole lot?”
Land here: Lock the chosen spot row with SELECT … FOR UPDATE SKIP LOCKED, or enforce one OPEN session per spot via partial unique index. Pair with Idempotency-Key on entry so gate retries do not create two tickets.
Pricing and time
You might say: “Fee equals hours times rate.”
Interview Push: “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 for session timestamps—not the gate device clock.
Overrides and messy reality
You might say: “Sensors always tell the truth.”
Interview Push: “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.
After each push, name one mechanism—row lock, idempotency key, unique constraint, or reconciliation job—not “we’ll handle it.”
What should stick
After reading this guide, you should be able to explain:
- Invariants first — One active session per spot; idempotent pay; monotonic lifecycle—before any class diagram.
- Truth vs display — Database owns occupancy; cache owns fast counts with reconciliation.
- Atomic claim —
FOR UPDATE SKIP LOCKEDor unique OPEN session stops two cars in 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 reconciliation paths.
Tell it in the room: “I define vehicle and spot rules and invariants first. Entry atomically claims one compatible spot and creates an OPEN session with an Idempotency-Key. Exit computes fee from server time, pays once idempotently, then releases the spot once. Availability displays use cached counts rebuilt from the spot state machine—not the other way around.”
Key Takeaways
- Invariants before classes — One spot, one open session, monotonic ticket, idempotent payment.
- Entry is the lock — One transaction claims a row; signs and cache are hints.
- SKIP LOCKED — Multiple lanes skip each other’s rows instead of blocking the whole lot.
- Idempotency-Key — Gate and payment retries return the same outcome.
- Exit order — Fee → pay → PAID → CLOSED → release spot once.
- Reconciliation — Counters and sensors follow session truth, not replace it.
- Shard by lot — Multi-lot scale partitions inventory; browse can federate.
Related Topics
- Airbnb Reservation System Design - Holds and preventing double bookings in reservation systems
- Vaccine Booking System Design - Last-slot contention and transactional inventory
- Rate Limiter System Design - Gate and API admission control patterns
- Booking Waitlist System Design - Returning spots to the pool safely
- Rate Limiting and Throttling - Edge rate limits for gate and plate lookup APIs
Quick revision notes
Use this as a last look before a mock interview, not as a substitute for the full guide.
Clarify first. Agree vehicle and spot types, allocation policy, payment timing, reservations vs walk-in, attendant overrides, and what you leave out of scope.
State invariants. One active session per spot; monotonic OPEN → PAID → CLOSED; idempotent payment; server UTC times.
Draw two paths. Entry: candidate query → FOR UPDATE SKIP LOCKED → OCCUPIED + OPEN session. Exit: fee → idempotent pay → release spot once.
Truth vs display. Cache counts for signs; claim always on primary; reconcile drift.
Name the dig. Reject read-free-then-write; one transaction one winner; image the two-cars-one-spot race.
Operate reality. Watch double-session violations, cache drift, payment dedup hits—not only entry 200 rate.
Practice next: Open Parking Lot on Practice System Design and run a timed attempt using the clarifying questions and entry/exit walkthrough above.
What interviewers expect
- Clarify rules first: vehicle types, spot types, allocation policy (nearest, lowest floor, zone), payment timing, reservations vs walk-in.
- Invariants before classes: at most one active session per spot; ticket lifecycle OPEN → PAID → CLOSED; payment idempotent on retries.
- Atomic claim on entry:
SELECT … FOR UPDATE SKIP LOCKEDon a candidate spot row, or a unique constraint on one OPEN session per spot. - Truth vs display: database owns occupancy; cached counters power signs and apps with periodic reconciliation.
- Entry and exit idempotency: gate retries must not create duplicate tickets; payment confirmation must not double-charge.
- Messy ops: lost tickets, attendant overrides, sensor drift—reconciliation paths, not “sensors always win.”
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
- How do you assign a spot without double booking?
- How do you handle concurrency at multiple entry gates?
- Walk through entry and exit with payment.
- How do you show availability when the lot is almost full?
- How does this scale to many independent parking lots?
Deep-dive questions and strong answer outlines
How do you assign a spot without two cars taking the same space?
I would not read “spot is free” and update later in a second request. Inside one short database transaction, pick a compatible candidate spot, lock that row with SELECT … FOR UPDATE SKIP LOCKED, flip status to OCCUPIED, and insert a ParkingSession with status OPEN. The second lane skips the locked row and tries the next candidate—or returns “lot full” if none remain. Optionally enforce a partial unique index: only one OPEN session per spot_id. Pair with an Idempotency-Key on entry so gate retries do not create two tickets for one vehicle.
How do multiple entry lanes coordinate on the same lot?
Each lane calls the same entry API backed by one authoritative database for that lot. Allocation is row-level, not “each gate keeps its own free list.” Fast “spots remaining” signs read from cached counters rebuilt from spot state, but the claim always hits the primary. If two lanes race, one transaction wins; the other gets the next spot or a clear “full” response. For very large garages, shard by lot_id so one mall does not serialize the whole city.
Walk through entry and exit with payment.
Entry: vehicle type and optional preferences → find compatible AVAILABLE spots → atomic claim → create OPEN session with server UTC entry_time → return ticket (session_id, spot_id, directions). Require Idempotency-Key so a timeout retry returns the same session. Exit: lookup by session_id or plate (lost ticket) → compute fee from entry_time with stated grace and rounding rules → create/confirm Payment with idempotency key → on success transition session OPEN → PAID → CLOSED → release spot to AVAILABLE exactly once. Do not release the spot before payment succeeds unless you explicitly allow pay-later with attendant override.
How do you handle a full lot signal at the gate?
The gate needs a fast answer, but “full” must be honest enough. Maintain derived counters per floor and spot type from the spot state machine (AVAILABLE, OCCUPIED, RESERVED, OUT_OF_SERVICE). Entry still does a real claim in a transaction—cached zero means “probably full,” not a guarantee. Reconcile counters hourly or on state change events; alert when abs(cached − derived) exceeds a threshold. Stop admitting when no compatible spot can be claimed, not only when a stale counter says zero.
How does this scale to many parking lots?
Shard by lot_id: each garage (or operator region) has its own service boundary and database partition. Cross-lot search is a read aggregator; entry and exit writes stay on the lot that owns the spots. Hot rows appear per popular lot at peak, not one national inventory row. Multi-lot operators share payment and reporting asynchronously; they do not merge spot assignment into one global counter.
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: Is this only an object-oriented design question?
A: Many interviews start with entities and state machines—that is fair. At InterviewCrafted depth, treat it as system design with concurrency: invariants, transactional assignment, idempotent gates, and how displays relate to truth. Say your classes aloud, then show the database mechanism that stops two cars in one spot.
Q: How do reservations work alongside walk-in entry?
A: Reservations hold a spot (or a spot type pool) with a TTL before arrival. Walk-in entry claims from AVAILABLE spots not held or expired. Use the same atomic claim path for both: RESERVED → OCCUPIED on check-in, or AVAILABLE → OCCUPIED for walk-in. A nightly job frees expired holds back to AVAILABLE. In the interview, separate hold inventory from walk-in inventory early so flash walk-in traffic does not steal reserved EV spots without policy.
Q: What if IoT sensors disagree with the database?
A: Treat sensor events as at-least-once hints, not the sole source of truth. The database reflects legal occupancy (ticket OPEN). If a sensor says occupied but DB says AVAILABLE, flag for attendant review—do not silently overwrite paid sessions. Reconciliation jobs compare sensor streams to session rows and open override tickets. Good teams measure divergence rate per floor.
Q: How does valet parking fit?
A: Valet is a parallel queue state machine: vehicle checked in at curb, spot assigned by attendant app using the same atomic claim API, keys tracked separately. Payment may happen at return, not at garage exit gate. Scope it as a different entry channel sharing spot inventory—not a second parking lot with duplicate spot rows.
Q: Should availability boards show exact spot numbers or only counts?
A: Counts per floor and type are enough for most garages and are cheaper to cache. Showing “Spot 214” on a sign is optional and increases stale display risk. If you show specific spots, treat it as a hint; entry still claims atomically. Say aloud which UX you picked and how often boards reconcile.
Q: What about dynamic pricing by hour or demand?
A: Store a pricing rules version on the session at entry (or re-rate at exit with a declared policy). Fee calculation uses server UTC timestamps, grace minutes, rounding rules, and a max-day cap for lost tickets. Payment idempotency keys include amount and pricing_version so a retry after a rule change does not double-charge or charge the wrong tier.
Practice interactively
Open the practice session to use the canvas and stages, then review AI feedback.