Design problems
Maps, mobility & logisticsMediumSDE-2 25 min

Design surge pricing

Asked at
OlaRapidoSwiggyUberZeptoZomato

The interviewer asks

“Design the surge pricing system for a ride-hailing app. When demand is higher than supply in an area, fares go up to balance the market. Every fare quote must use the current multiplier for the rider's location, and the system must be fast, stable and fair.”

What a strong answer looks like

A strong answer treats surge as a streaming analytics problem feeding a low-latency lookup. Demand signals (fare quotes, ride requests, unfulfilled requests) and supply signals (available drivers, drivers about to finish trips) are counted per hexagonal cell (H3) per vehicle type over short sliding windows by a stream processor. A pricing job computes a multiplier per cell every 30-60 seconds from the supply-demand ratio, then smooths it over time and across neighbouring cells, applies caps from policy and regulation, and publishes a versioned snapshot to a cache. The quote service reads the multiplier for the pickup cell in under a millisecond and locks it into a signed fare quote, so the rider pays what they saw. The deep dives are the signals and formula, smoothing and stability, quote consistency, and safety controls (caps, kill switches, emergencies), plus how to test changes with experiments.

Concepts to mention

Demand and supply signals per H3 cell and vehicle typeSliding windows in a stream processorConfigurable multiplier curveSpatial smoothing over neighbouring cellsTemporal smoothing, step limits and hysteresisAtomic per-city versioned snapshotSigned, expiring, single-use fare quoteCaps, overrides and kill switchFreshness guard and decay to 1.0Switchback experiments

1 Requirements

Functional

  • Compute a surge multiplier per area (cell) and vehicle type from current demand and supply
  • Apply the multiplier in fare quotes; the rider pays the quoted fare if they book within the quote's validity
  • Show drivers a heat map of surge areas to move supply toward demand
  • Respect caps: city-level maximums, regulatory limits, and emergency or disaster overrides
  • Let operations set manual overrides per area and time (for example, a stadium event) and a global kill switch
  • Log every multiplier and quote for audit, disputes and analysis

Non-functional

  • Multiplier lookup during quoting p99 under 5 ms
  • Multiplier freshness: recomputed every 30-60 s; signal lag under 30 s
  • Stable prices: no flicker between adjacent minutes or across a street boundary
  • Handle ~200k input events/s (location pings, app opens, quotes, requests) and ~5k quotes/s
  • If surge computation fails, quotes continue with the last known value (with a max age) or no surge
  • Fully reproducible: given the inputs and config version, recompute the same multiplier

Out of scope (say it out loud)

  • Base fare and routing calculation
  • Driver incentives and bonuses (mention only)
  • Dispatch and matching

2 Estimates

Cells~100k active cell × vehicle type pairsH3 resolution 8 cells are ~0.7 km²; a large city like Bengaluru (~740 km²) has ~1,000 cells; ~30 big cities ≈ 30k cells × 3 vehicle types ≈ 100k
Input events~200k/s peak~150k/s driver location pings (gives supply) + ~20k/s app opens and quotes + ~1k/s requests
Multiplier updates~2k/s100k pairs recomputed every 60 s ≈ 1.7k writes/s
Snapshot size~2-5 MB per city set100k entries × ~30 bytes (cell id, type, multiplier, version) ≈ 3 MB; small enough to hold in every quote node's memory
Quote lookups~5k/s peakRiders check prices several times per booked ride; one multiplier lookup per vehicle type per quote
Audit log~150M rows/day100k pairs × 1,440 minutes ≈ 144M multiplier rows/day; store in a columnar store (ClickHouse / S3 Parquet)

3 API

EndpointWhat it does
gRPCSurge.GetMultiplier(lat, lng, vehicle_type)Returns multiplier, cell_id, snapshot_version; served from in-memory snapshot
POST/v1/fare-quotesQuote service includes surge multiplier, version and expiry in a signed quote
GET/v1/driver/heatmap?bbox=Driver app heat map of surge cells for the visible area (cached, refreshed every minute)
POST/v1/admin/surge-overridesOps sets cap, floor or fixed multiplier for a polygon and time window; audited, needs approval
POST/v1/admin/surge-killswitchDisable surge for a city or nationally (for example during a disaster)

4 Data model

demand_supply_windowsFlink keyed state (RocksDB)
key (city, h3_cell, vehicle_type) → sliding counts: quotesrequestsunfulfilledopen_driversdrivers_finishing_soon

1-minute buckets over 5-10 minute windows; results emitted every 30 s.

surge_snapshotRedis (versioned key per city) + in-memory on quote nodes
cityversioncomputed_atmap (h3_cell, vehicle_type) → multiplier

Published as a whole snapshot per city per cycle, so quotes never mix two versions. Nodes poll or subscribe for the new version.

surge_configPostgres
cityvehicle_typecurve parametersmax_multipliermin_stepsmoothing paramsversion

Versioned config; each snapshot records the config version used.

overridesPostgres + snapshot
override_idpolygon (H3 cell set)vehicle_typemode (cap, floor, fixed, off)starts_atends_atcreated_byapproved_by

Applied as the last step of the pricing job.

surge_historyClickHouse / S3 Parquet
tscitycellvehicle_typedemandsupplyraw_multiplierfinal_multiplierconfig_versionsnapshot_version

For disputes ('why was it 1.8x at 9:02?'), research and model training.

quotesKafka → S3, Redis for active quotes
quote_idrider_idcellmultipliersnapshot_versionfareexpires_atconverted

Conversion rate per multiplier is the key signal for tuning (price elasticity).

5 Architecture

CLIENTSEDGESERVICESASYNCDATAget farelocation pingsmultiplierquote eventscounts per cellconfig, overridespublish snapshotload new versionhistoryheat mapDriver appRider appAPI gatewaySurge lookup(in-memory)Quote serviceOps console /overridesKafka (pings, quotes,reqs)Flink demand/supplyPricing job (per city)Redis surge snapshotsPostgres (config)ClickHouse history
ClientEdge / gatewayService / workerQueue / streamCacheDatabase / storageAI / modelThird partyrequestasync (event / queue)

Hover a box to highlight its connections.

6 Request flows

Compute surge for a city

  1. 1Driver pings and rider events flow into Kafka, partitioned by city.
  2. 2Flink maps each event to its H3 cell and vehicle type and updates sliding counts: demand = requests + weighted quotes + unfulfilled requests; supply = available drivers + drivers finishing a trip nearby within ~5 minutes.
  3. 3Every 30-60 s, the per-city pricing job reads the latest counts for all cells in the city.
  4. 4For each cell it computes a raw multiplier from the demand/supply ratio using the city's curve (for example, no surge below ratio 1.2, rising steadily above it).
  5. 5It smooths: spatially (blend with neighbouring cells' ratios using H3 rings) and over time (exponential moving average, max step up or down per cycle).
  6. 6It applies rules in order: round to 0.1 steps, city and regulatory caps, active overrides, and the kill switch.
  7. 7It writes the whole city snapshot with a new version to Redis and appends rows to history; surge lookup nodes load it within seconds.

Rider gets a quote and books

  1. 1Rider at a pickup point asks for fares; the quote service calls Surge.GetMultiplier for each vehicle type.
  2. 2The lookup converts lat/lng to the H3 cell and reads the in-memory snapshot: 1.4x for autos, 1.3x for cars, version 81,224.
  3. 3Quote service computes fare = base fare from distance and time × multiplier (plus fees), signs a quote with the multiplier, version and a 3-5 minute expiry.
  4. 4Rider books within the validity; the trip uses the quoted fare even if surge has changed since. After expiry, the app requests a fresh quote and shows the new price clearly.

7 Deep dives

Signals and the multiplier formula

Surge is only as good as its signals. Demand is not just booked rides: if prices are high, people stop booking, so bookings alone would make surge fall and then rise again. Use intent signals too: app opens and fare quotes in the cell (weighted lower than requests), and unfulfilled requests (a strong signal that supply is short). Supply is available drivers in the cell plus drivers whose current trip ends nearby in the next few minutes, because they will soon be free. Normalise both to the same window (say the last 5 minutes) and compute the ratio. Map ratio to multiplier with a configured curve: flat at 1.0 up to a threshold, then rising, then capped. The curve's shape is a business decision tuned by experiments using price elasticity (how conversion falls as multiplier rises) and how quickly drivers respond. More advanced systems predict demand 10-15 minutes ahead (time of day, weather, events, train arrivals) and price on the forecast, so drivers are already moving when the rush starts. In the interview, define demand and supply carefully and say that the formula is configurable and tested, rather than inventing a perfect equation.

Stability: smoothing in time and space

A naive per-cell, per-minute ratio flickers: one cell is 2.1x, the next cell across the road is 1.0x, and a minute later they swap. Riders learn to walk 100 m or wait a minute; drivers chase cells that vanish. Three tools fix this. Spatial smoothing: compute each cell's value as a weighted blend of its own ratio and its neighbours' (H3 rings k=1, k=2), so prices change gradually across an area; very small cells also have too few events to be statistically reliable, and blending fixes that. Temporal smoothing: use an exponential moving average and limit the change per cycle (for example at most +0.3x or -0.2x per minute), with hysteresis so the price does not toggle around a threshold. Rounding: publish in steps of 0.1x or 0.25x. Hex cells help because all six neighbours are the same distance, unlike square geohash cells where diagonal neighbours are farther. Too much smoothing makes surge react slowly to a real rush, so tune the trade-off per city and measure both 'time to respond' and 'flicker rate'.

Quote consistency: pay what you saw

The rider must pay the price shown, even if the multiplier changes between the quote and the booking. Lock the multiplier into the quote: the quote service signs (HMAC) a token containing rider_id, pickup cell, vehicle type, multiplier, fare, snapshot version and expiry. When the rider books, the trip service verifies the signature and expiry, and uses the quoted fare. This needs no shared state on the booking path and works across any node. Make quotes single-use (mark quote_id used in Redis with SET NX) so one cheap quote cannot be reused for many rides. Within a single quote request, all vehicle types must use the same snapshot version; that is why the pricing job publishes a whole-city snapshot atomically rather than writing cells one by one. Expiry should be short (3-5 minutes): long enough to decide, short enough that a rider cannot hold a cheap price through a rush. If the user's pickup point moves significantly before booking, re-quote. Keep the snapshot version in the trip record, so support can answer 'why did I pay 1.6x' by looking at exactly what was in force.

Safety: caps, overrides and kill switches

Surge has real-world consequences, so controls are part of the design, not an afterthought. Caps: a per-city maximum and per-vehicle-type maximum in config, plus regulatory caps; in India, aggregator guidelines limit how far fares can go above the base fare, so the cap must be configurable per state. Emergencies: during floods, terror incidents or disasters, many companies cap or disable surge; the kill switch must work in seconds and must not depend on the pricing pipeline being healthy, so the lookup service checks a separate flag before using the snapshot. Overrides: operations can cap, floor or fix multipliers for a polygon and a time window (for example around a stadium after a match), with approval and an audit trail. Freshness guard: if the snapshot is older than a few minutes, decay to 1.0 rather than keep an old high multiplier forever. Fairness checks: run analyses to make sure surge does not systematically affect some neighbourhoods unfairly. Finally, test changes with experiments: switchback tests (alternate configs by time slot within a city) work better than user-level A/B tests, because riders and drivers in the same market affect each other.

8 Trade-offs

Stream processing vs periodic batch query

A periodic SQL query over the last 5 minutes of events (in ClickHouse) is simpler to build and debug and is fine for one city. Flink keeps windowed counts incrementally and scales to hundreds of thousands of events per second with low lag. Use Flink at national scale; start with batch queries for a small launch.

Fine cells vs coarse cells

Small cells (H3 res 9) target surge precisely but have noisy counts and create sharp borders. Large cells (res 7) are stable but blunt: a whole neighbourhood surges for one busy mall. Compute at a fine resolution and smooth over neighbours, or choose resolution by density (finer in city centres).

Multiplier vs additive surge

A multiplier hits long trips much harder than short ones. An additive surge (a fixed extra amount per trip) is easier to understand and fairer for long rides but may not attract drivers to short trips. Many apps now show only the final upfront fare and hide the mechanism; the back end can support both.

9 Bottlenecks

Location ping volume

At 10x drivers, pings become millions per second, most of which do not change cell. Have the location service emit only 'driver entered/left cell' and status changes to the surge pipeline instead of raw pings; supply counts are then cheap to maintain.

Snapshot distribution

Thousands of quote nodes loading a new snapshot every minute from Redis is a burst. Publish per-city snapshots as compressed blobs, notify via pub/sub, and let nodes fetch with jitter; or push via a CDN-like config distribution system.

10 Failure handling

Flink job or pricing job down

Quote nodes keep using the last snapshot. After a maximum age (say 5 minutes), they decay multipliers toward 1.0 and alert. Pricing never blocks quoting.

Bad config pushed (curve too steep)

Multipliers spike across a city. Guards in the pricing job compare the new snapshot to the previous one (for example, more than 30% of cells jumping by over 1x) and refuse to publish, keeping the last good snapshot. Configs are versioned so rollback is instant.

Kafka lag

Signals arrive late, so surge lags reality. Alert on lag; the pricing job checks event-time freshness of its inputs and holds or decays multipliers if inputs are older than a threshold.

11 What impresses

  • Defining demand to include unfulfilled requests and quotes, not just bookings
  • Explaining flicker and border effects and fixing them with spatial and temporal smoothing
  • Locking the multiplier into a signed quote with a snapshot version
  • Mentioning switchback tests because riders and drivers interfere across user-level A/B groups
  • Treating caps, regulation and emergency kill switches as first-class requirements
  • Keeping quoting independent of the pricing pipeline's health

12 Mistakes

  • Computing surge synchronously inside the quote request
  • Using only bookings as demand
  • No smoothing, causing prices to jump between adjacent streets and minutes
  • Charging a different price at booking than the one shown
  • Forgetting caps, regulation and emergencies
  • No way to explain a past multiplier to a complaining rider

13 Follow-ups

Q1How would you predict surge 15 minutes ahead?
Q2How do you show drivers where to go without everyone rushing to the same cell?
Q3How would you test a new surge curve safely?
Q4How do you handle a big concert ending at a known time?
Q5How would surge work for food delivery fees?
Q6How do you stop drivers from gaming surge by going offline together?

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