Skip to main content

Design a Parking Garage

Premium

Neamah asks Tim to design a reservation and payment system for a parking garage.

You're asked to design the system behind a multi-level parking garage, or a network of them. Cars arrive at an entry gate, are admitted if space is available, park somewhere, and pay on the way out based on how long they stayed. The system tracks occupancy in real time, supports different spot types such as compact, standard, oversized, electric, and accessible, and shows drivers where space is available.

The inventory is only a counter, but physical gates modify it even when the network is unavailable. Payment also happens minutes or hours after entry. Let's prioritize offline gate behavior and reconciliation over scaling a small workload.

Clarify the requirements

  • One garage or many? A single garage is a much smaller problem than a city-wide network with a shared reservation and payment layer. Ask which one, because it determines whether this is a single-database design.
  • Are spots assigned or is it free parking? Assigning a specific spot needs per-spot state and a way to detect that a driver parked somewhere else. Counting by type is usually enough and tolerates real-world drift better.
  • Can drivers reserve in advance? Reservations turn a counter into future-dated inventory, which is closer to hotel booking and roughly doubles the scope.
  • How is pricing determined? Hourly rates, daily maximums, and rate changes at certain hours all show up in real garages, and they're where the billing logic gets interesting.
  • What happens when the network is down? Clarify how gates should behave because cars continue arriving and leaving during an outage.

Assume: a network of garages, counted capacity by spot type, no advance reservations, tiered hourly pricing with a daily cap.

Back-of-envelope numbers

  • Capacity: 100 garages×1,000 spots=100k spots100 \text{ garages} \times 1{,}000 \text{ spots} = 100\text{k spots} total
  • Entry and exit events: 100k spots×3 turnovers/day×2 events=600k events/day≈7/sec100\text{k spots} \times 3 \text{ turnovers/day} \times 2 \text{ events} = 600\text{k events/day} \approx 7/\text{sec}
  • Peak, concentrated at commute hours: roughly 100 events/sec100 \text{ events/sec}
  • Occupancy lookups from a mobile app: 1M/day≈12/sec1\text{M/day} \approx 12/\text{sec}
  • Active sessions at any moment: at most 100k100\text{k}, so the working set is tiny

Every number here is small. Spend your time on physical constraints and offline operation rather than sharding.

High-level architecture

Entry gate

① Gate controller

Exit gate

② Garage service

③ Occupancy store

④ Session store

⑤ Billing service

⑥ Payment provider

⑦ Driver app

Components
  1. Gate controller. On-premises, holds a local copy of occupancy, and can operate the barrier without the backend.
  2. Garage service. The authoritative owner of occupancy counts and parking sessions.
  3. Occupancy store. A counter per garage per spot type, which is the inventory being claimed.
  4. Session store. One open record per parked vehicle, holding entry time and spot type.
  5. Billing service. Computes the fee from the session duration and the rate schedule.
  6. Payment provider. External, called at exit rather than at entry.
  7. Driver app. Reads occupancy to show availability, and can hold a payment method on file.
Gate controllers hold their own local state and reconcile with the backend, so a car can always be admitted or released even when the network is unavailable.

Deep dive 1: occupancy as counted inventory

Modeling every physical spot as a row is tempting and usually wrong. Unless the garage physically enforces which spot a driver takes, per-spot state drifts from reality the moment someone parks in a different space than the one they were assigned.

The model that holds up is a counter per garage per spot type:

garage_idspot_typetotaloccupied
12standard700688
12compact240240
12ev6041

Admission is a conditional increment, which is the same atomic claim the transactional workflows pattern uses everywhere:

SQL
UPDATE occupancy SET occupied = occupied + 1 WHERE garage_id = ? AND spot_type = ? AND occupied < total;

Zero rows updated means the garage is full for that type, and the gate stays down. One row updated means the car is admitted and a session is opened. There's no interval between checking and claiming for two cars at two entrances to slip through.

Add spot type substitution as a refinement. A compact car can use a standard space, so define an ordered fallback for each vehicle type and attempt each capacity claim in that order.

Deep dive 2: the gate has to work offline

Gate controllers sit in concrete structures with unreliable connectivity, and cars keep arriving regardless. A design where a network partition traps every vehicle in the garage is not deployable, so this is the constraint that shapes the local architecture.

The resolution is to give the controller enough local state and enough authority to keep operating, then reconcile afterward.

  • Cache occupancy locally, updated continuously while the network is up, so the controller can decide admission on its own.
  • Always allow exit. A car leaving is never a correctness problem, and trapping people is unacceptable in a way that a small billing error is not. If the fee can't be computed or charged, record the exit and settle later.
  • Queue events locally and replay them when connectivity returns, since entries and exits are facts that already happened rather than requests that can be rejected.
  • Fail permissive on entry within a margin. Admitting a few cars beyond capacity during an outage is recoverable; refusing everyone is a closed garage.

Reconciliation then corrects the counters from the queued events, which will occasionally show occupancy above the physical total. That's an accurate reflection of what happened, and the counter converges as those cars leave.

Deep dive 3: the parking session and billing at exit

The claim is made at entry and the money is collected at exit, which is the opposite order from most transactional systems, and it means the system extends credit for the duration of the stay.

A session is opened at entry with the vehicle identifier, garage, spot type, and entry timestamp, and it stays open until the car leaves. At exit, the fee comes from the elapsed time against the rate schedule, which in a real garage is more involved than an hourly multiplication:

  • Tiered rates where the first hour costs more than subsequent ones.
  • A daily maximum, so a long stay is capped.
  • Time-of-day and day-of-week variation, where a session spanning a rate change is billed across both periods.
  • Grace periods, where a driver leaving within a few minutes of entry pays nothing.

Storing the rate schedule as data with effective dates, rather than as code, is what keeps this manageable, and it means a session can be re-priced correctly if a dispute comes up later.

The unpaid exit needs an explicit failure path. A card declines, the driver has no payment method, or the terminal is offline. Since the alternative is holding a car at a barrier, the practical answer is to release the vehicle and pursue payment afterward through the license plate and the account on file, recording the session as unsettled rather than closed. The physical system cannot simply refuse.

Deep dive 4: making entry and exit idempotent

Gates produce duplicate events. A sensor fires twice, a controller replays its queue after reconnecting, and a barrier cycles when a car reverses and re-enters. Each duplicate that reaches the counter unchecked costs a real parking space.

The defense is to make the session, rather than the event, the unit of truth. An entry event carries an identifier, either a ticket number or a plate reading with a timestamp window, and opening a session is conditional on there being no open session for that vehicle in that garage. A second entry event for a car already inside is recognized and dropped rather than incrementing the counter again.

Exit works the same way in reverse. Closing an already-closed session is a no-op that returns the original fee, which also means a driver who taps twice at the payment terminal isn't charged twice.

Two more mechanisms that keep the counter honest over time:

  • Periodic reconciliation against a physical count, whether from lane sensors, camera counts, or a manual audit, since small drift is inevitable in a system driven by hardware.
  • Stale session cleanup for cars that entered and never exited, usually because an exit event was lost. Closing sessions older than some threshold prevents the occupancy count from ratcheting upward permanently.

Common pitfalls

  • Modeling individual spots when the garage doesn't enforce them. Per-spot state drifts as soon as a driver parks somewhere other than where they were assigned.
  • Checking availability and then incrementing separately. Two entry lanes can admit the last car simultaneously.
  • A gate that depends on the network. An outage either traps cars inside or closes the garage entirely.
  • Blocking exit on payment failure. The physical consequence is a queue of trapped drivers, which no billing accuracy justifies.
  • Non-idempotent gate events. Duplicate sensor readings and queue replays silently consume capacity.

Leveling signals

Mid-levelTracks occupancy as counts by spot type, opens a session at entry and closes it at exit, and computes the fee from elapsed time.
SeniorAdmits cars with a conditional increment so two lanes can't both take the last spot, supports spot-type fallback, and stores the rate schedule as data with tiers and a daily cap.
Staff+Designs the gate to operate and reconcile offline, always allowing exit and settling unpaid sessions afterward. Makes entry and exit idempotent against duplicate sensor events, and reconciles counters against physical counts with stale-session cleanup.
Design TicketmasterHard

Sell assigned seats under extreme contention, with holds and a waiting room.

Design E-commerce CheckoutHard

Take a multi-item cart through reservation, payment, and fulfillment without overselling.