Design problems
Maps, mobility & logisticsMediumSDE-2 35 min

Design EV charging station booking

Asked at
GoogleOlaUber

The interviewer asks

“Design an app where EV drivers find nearby charging stations, see which chargers are free, book a time slot, start charging from the app and pay for the energy used. Stations belong to many operators and the chargers talk to your backend over the network.”

What a strong answer looks like

A strong answer sees two very different halves. The consumer half is a booking system: slots per connector, no double booking, holds with expiry, no-show handling, and payment by energy used. The device half is an IoT system: thousands of chargers keep persistent WebSocket connections speaking OCPP (Open Charge Point Protocol), sending status changes, heartbeats and meter values, and receiving remote start/stop commands. Live availability shown in the app comes from charger status, not from bookings alone, because walk-in users, faults and overstays change reality. The booking engine uses a time-range exclusion constraint per connector, a grace period, and reconciles bookings with real charger state. Payment uses a wallet or pre-authorised amount, with the final bill computed from meter readings. Deep dives: OCPP connection management, slot booking with walk-ins and overstays, session billing correctness, and roaming across operators (OCPI).

Concepts to mention

OCPP over WebSocketstateful gateway with routing tableidempotent handling of charger retriesexclusion constraint on time rangesgrace period and no-show handlinglive status vs booking calendarbilling from meter start/stop readingspre-authorisation then captureOCPI roaming and CDR reconciliationoffline chargers queue and replay

1 Requirements

Functional

  • Find stations near a location or along a route, filtered by connector type (CCS2, Type 2, Bharat DC-001) and power (kW).
  • See live status per connector: available, charging, reserved, faulted, offline.
  • Book a connector for a time slot (e.g. 30-90 minutes) with a grace period for late arrival.
  • Start and stop charging from the app (or by scanning a QR code on the charger); see live kWh, cost and state of charge.
  • Pay by wallet, UPI or card; bill by energy (kWh) plus idle/overstay fees; refunds for failed sessions.
  • Operators (CPOs) manage stations, tariffs and see utilisation and faults.

Non-functional

  • Scale: ~100k connectors across many operators, ~2M registered drivers, ~300k sessions/day.
  • No double booking of a connector for overlapping times.
  • Remote start command reaches the charger and responds within ~5 s p95.
  • Live status in the app is at most ~30 s stale.
  • Billing must be exact and auditable from meter readings; no lost sessions even if the charger goes offline.
  • Availability 99.9% for app and charger connections; chargers keep working offline for authorised users.

Out of scope (say it out loud)

  • Charger firmware and smart grid load management in depth (mention load limits only).
  • Route planning with battery modelling beyond 'stations along route'.
  • Hardware installation and field service workflows.

2 Estimates

Charger connections~60k WebSocket connections100k connectors, ~1.7 connectors per charge point ≈ 60k charge points, each one persistent OCPP connection.
Heartbeats and status~1k msgs/sHeartbeat every 5 min: 60k ÷ 300 = 200/s; status changes and other messages ~5x more at peak.
Meter values~400/s at peak~25k concurrent sessions at peak × 1 MeterValues per 60 s ≈ 420/s.
Sessions~300k/day ≈ 3.5/s avg, ~15/s peak100k connectors × ~3 sessions/day.
Bookings~100k/day ≈ 1-5/sAbout a third of sessions are pre-booked; the rest are walk-ins with QR scan.
App reads~2k/s peakMap and station views by drivers; availability served from a cache.
Meter data storage~2 GB/day~25M meter samples/day × ~80 bytes; kept for billing audit and analytics.

3 API

EndpointWhat it does
GET/v1/stations?lat=&lng=&radius_km=&connector=&min_kw=Nearby stations with live connector status and tariff.
GET/v1/connectors/{id}/slots?date=Free and booked time ranges for a connector.
POST/v1/bookingsBook {connector_id, start, end}; Idempotency-Key; holds a pre-authorised amount.
POST/v1/sessionsStart charging {connector_id, booking_id?}; sends RemoteStartTransaction to the charger.
POST/v1/sessions/{id}/stopStop charging; sends RemoteStopTransaction.
SSE/v1/sessions/{id}/liveLive kWh, power, cost and state of charge.
WS/ocpp/{charge_point_id} (OCPP 1.6J / 2.0.1)Charger connection: BootNotification, Heartbeat, StatusNotification, StartTransaction, MeterValues, StopTransaction; server sends remote commands.

4 Data model

stations / connectorsPostgres + PostGIS
station_idcpo_idlocation (geography)amenities; connector_idcharge_point_idtypemax_kwtariff_id

Geo index for nearby search; small data (100k rows), cached heavily.

bookingsPostgres
booking_idconnector_iddriver_idslot tstzrangestatus (HELD|CONFIRMED|ACTIVE|NO_SHOW|CANCELLED|COMPLETED)grace_untilidempotency_key

EXCLUDE USING gist (connector_id WITH =, slot WITH &&) WHERE status in active states: the database forbids overlaps.

sessionsPostgres
session_idtransaction_id (OCPP)connector_iddriver_idbooking_idmeter_start_whmeter_stop_whstarted_atstopped_atstatusamount

Billing truth comes from meter_start and meter_stop readings, not from time.

meter_valuesTimescaleDB or ClickHouse
session_idtsenergy_whpower_wsoc_pct

Time series for live view, disputes and analytics.

connector_statusRedis
status:{connector_id} → {statesincesession_idbooking_id}

Live state for the map; written by the OCPP gateway, TTL refreshed by heartbeats so silent chargers show offline.

ocpp_routingRedis
cp:{charge_point_id} → gateway_node_id

Which gateway node holds the charger's socket, for routing remote commands.

wallet_ledgerPostgres
entry_iddriver_idamounttype (preauth|capture|release|refund)ref_id

Double-entry style ledger; balances derived, holds released on cancel.

5 Architecture

CLIENTSEDGESERVICESASYNCDATAEXTERNALWebSocketlive statusEXCLUDE constraintremote start/stopstatus, metersstart/stop/meterpreauth, captureexpire, no-showroamingDriver appChargers (OCPP)API gatewayWSS load balancerBooking serviceSession + billing svcStation search svcOCPP gateway nodesNo-show / expiry jobsKafka (charger events)Postgres + PostGISTimescaleDB (meters)Redis (status,routing)Payment gatewayOther CPOs (OCPI)
ClientEdge / gatewayService / workerQueue / streamCacheDatabase / storageAI / modelThird partyrequestasync (event / queue)

Hover a box to highlight its connections.

6 Request flows

Book a slot

  1. 1Driver picks a connector and a 60-minute slot from 18:00; the app sends POST /bookings with an Idempotency-Key.
  2. 2Booking service places a pre-authorisation (e.g. ₹500) on the wallet or card.
  3. 3It inserts the booking as CONFIRMED with slot [18:00, 19:00) and grace_until 18:10; the exclusion constraint rejects the insert if another active booking overlaps.
  4. 4On conflict, it releases the pre-auth and returns the nearest free slots.
  5. 5Shortly before 18:00, if the charger supports it, the session service sends ReserveNow so the connector shows 'reserved' and refuses walk-ins.

Start and finish charging

  1. 1Driver plugs in and taps Start; the session service checks the booking or wallet balance, then looks up which OCPP gateway node holds this charger in Redis.
  2. 2It sends RemoteStartTransaction through that node and waits for the charger's response (Accepted/Rejected) with a ~10 s timeout.
  3. 3The charger sends StartTransaction with meter_start; the gateway publishes it to Kafka; the session service creates the session with OCPP transaction_id and links the booking.
  4. 4MeterValues arrive every 60 s; the session service updates the live view and stores samples.
  5. 5On stop (app, car full, or cable removed), the charger sends StopTransaction with meter_stop; the bill = (meter_stop - meter_start) × tariff + idle fee; the pre-auth is captured for that amount and the rest released.

No-show

  1. 1At grace_until (18:10) a scheduled job checks for an active session on that connector for this booking.
  2. 2If none, it marks the booking NO_SHOW, sends CancelReservation to the charger, charges a small no-show fee per policy and releases the rest of the pre-auth.
  3. 3The connector becomes bookable again and the live status updates.

7 Deep dives

OCPP gateway: tens of thousands of persistent charger connections

Chargers connect to the backend, not the other way around, over WebSocket (OCPP 1.6J is still most common, 2.0.1 is growing), usually behind cellular modems with flaky connectivity. Run a fleet of stateful gateway nodes behind a TCP/TLS load balancer; each node holds ~10-20k sockets. The gateway only translates protocol: it validates messages, answers Heartbeat and BootNotification itself, publishes StatusNotification, StartTransaction, MeterValues and StopTransaction to Kafka keyed by charge_point_id (preserving per-charger order), and writes live status to Redis. It records which node owns which charger in Redis so the session service can route a RemoteStartTransaction to the right node, via an internal RPC or a per-node command queue. OCPP calls are request/response with a unique message ID; the gateway must correlate responses and time out. Since the charger retries messages it did not get a response for, every handler must be idempotent: a duplicate StartTransaction must not create a second session. Firmware differs across vendors, so keep vendor quirks in adapters, not in business services. Reconnect storms after a gateway deploy need slow draining and jittered reconnects.

Booking vs reality: walk-ins, faults and overstays

A booking calendar alone gives wrong answers, because the physical connector can be occupied by a walk-in driver, a car that finished charging but was not moved, or be faulted. Combine both sources. Bookings live in Postgres with an exclusion constraint on (connector_id, slot) so overlapping bookings are impossible even under races; this is simpler and safer than application locks. Live state comes from the charger. Before a booked slot, send ReserveNow so the charger itself blocks other users; if the charger does not support reservations, stop selling walk-in sessions on that connector ~15 minutes before the booking, and cap walk-in sessions so they end in time. If the connector is faulted or occupied at slot start, the system automatically moves the booking to another free connector at the same station and notifies the driver, or refunds. Overstays are the real-world problem: apply an idle fee per minute after charging completes, notify the driver, and leave a buffer of ~10 minutes between bookings. Show the driver honest information: 'booked slots' and 'live status' separately.

Billing correctness from meter readings

Money must come from the meter, not from the app's timer. The OCPP StartTransaction carries meter_start (Wh) and StopTransaction carries meter_stop; energy = stop - start, priced by the station's tariff (per kWh, plus idle fees and GST). Chargers sometimes go offline mid-session and send StopTransaction hours later from their local queue; the session stays ACTIVE until then, and the bill is finalised when the stop arrives. If a charger never reports a stop, a reconciliation job uses the last MeterValues reading after a timeout and flags the session for review. Use a pre-authorisation at booking or start (card hold or wallet block) and capture the exact amount later, so a driver cannot run up an unpaid bill. All money movements are rows in a ledger with idempotency keys (session_id + type), so retries never double charge. Store raw meter samples for disputes, and use signed meter values where the hardware supports them (Eichrecht-style signing in Europe) for tamper evidence.

Many operators: roaming with OCPI

Drivers want one app for every network, but stations belong to many charge point operators (CPOs). Two integration types exist. For stations you operate or that connect directly to you, you speak OCPP. For stations on other networks, you speak OCPI (Open Charge Point Interface), which lets an e-mobility service provider (your app) and a CPO exchange locations, live status, tariffs, tokens, sessions and charge detail records (CDRs). Pull their station and status data into your station catalogue (and accept their push updates), and send start/stop commands through their OCPI command module. Their status freshness varies, so show a confidence or 'last updated' hint. Settlement uses CDRs: the CPO bills you per session and you bill the driver, so reconcile CDRs against your session records daily and alert on mismatches. This design keeps your booking and wallet logic the same regardless of whether the charger is yours or a partner's: a provider interface with OCPP and OCPI implementations.

8 Trade-offs

Exclusion constraint vs Redis locks for slots

Postgres EXCLUDE USING gist with a tstzrange rejects overlapping bookings atomically, survives crashes and needs no lock expiry logic. Redis locks per slot work only with fixed slot sizes and add failure modes. Booking QPS is low (single digits per second), so the database constraint is the right tool.

Fixed slots vs free time ranges

Fixed 30-minute slots are simple to display and reason about. Free ranges use chargers better but make the UI and fragmentation harder. Use flexible ranges in storage (tstzrange) and present 15-minute aligned choices in the UI.

Booking-only vs walk-in friendly stations

Allowing only bookings wastes capacity when people do not book; walk-ins only frustrate travellers on highways. Mix both: cap bookings at e.g. 60% of connector-hours per station, keep the rest walk-in, and adjust per station by utilisation.

9 Bottlenecks

Gateway reconnect storms

After a network blip, 60k chargers reconnect within seconds and each sends BootNotification and StatusNotification for every connector. Scale gateway nodes with headroom, answer boots quickly, rate limit status processing, and let the load balancer spread connections. Ask vendors to configure jittered reconnect intervals.

Highway stations during long weekends

Demand concentrates on a few highway stations (e.g. Mumbai-Pune Expressway) on holiday weekends. Booking alone does not create capacity; show queue length and predicted wait, suggest the next station along the route, and enforce idle fees strictly.

10 Failure handling

Charger loses connectivity mid-session

Charging continues locally. The charger queues MeterValues and StopTransaction and sends them on reconnect. The app shows 'charger offline, session continues' with the last reading. Billing is finalised when the stop arrives or by reconciliation after a timeout.

Remote start command times out

The session is left in PENDING_START. If a StartTransaction arrives later, the session proceeds; if nothing arrives within ~60 s, mark it failed and release the pre-auth. Tell the driver to retry or use the charger's RFID/QR fallback.

Gateway node crashes

Its chargers reconnect to other nodes within seconds; the routing table in Redis is updated on reconnect. In-flight commands fail and are retried by the session service with the same message idempotency.

11 What impresses

  • Knows that chargers speak OCPP and connect outbound to the backend over WebSocket.
  • Uses a Postgres exclusion constraint to make overlapping bookings impossible.
  • Reconciles bookings with real charger state and plans for overstays and faults.
  • Bills from meter readings with pre-auth and capture, and handles delayed StopTransaction.
  • Mentions OCPI for roaming across operators and CDR reconciliation.
  • Designs remote commands with routing to the node holding the socket and with timeouts.

12 Mistakes

  • Treating the problem as only a calendar booking and ignoring the devices.
  • Billing by elapsed time instead of energy measured by the meter.
  • Assuming chargers are always online.
  • Polling every charger over HTTP for status.
  • Using application-level check-then-insert for slots without a constraint or lock.
  • Not handling the car that finished charging but stays plugged in.

13 Follow-ups

Q1How do you limit total power when a site's grid connection allows only 200 kW for 6 fast chargers?
Q2How do you route a remote stop command to the right gateway node?
Q3A driver disputes a bill. What data do you show?
Q4How would you predict wait time at a station without bookings?
Q5How would you support fleet customers with monthly invoicing?
Q6How do you upgrade from OCPP 1.6 to 2.0.1 across vendors?

Saved on this device. Create a free account to keep it on every device.

Practise this with Sam

A real mock interview, out loud, with an honest scorecard. Free to start.

Start free