Design surge pricing
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
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
3 API
| Endpoint | What it does |
|---|---|
gRPCSurge.GetMultiplier(lat, lng, vehicle_type) | Returns multiplier, cell_id, snapshot_version; served from in-memory snapshot |
POST/v1/fare-quotes | Quote 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-overrides | Ops sets cap, floor or fixed multiplier for a polygon and time window; audited, needs approval |
POST/v1/admin/surge-killswitch | Disable 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_soon1-minute buckets over 5-10 minute windows; results emitted every 30 s.
surge_snapshotRedis (versioned key per city) + in-memory on quote nodescityversioncomputed_atmap (h3_cell, vehicle_type) → multiplierPublished as a whole snapshot per city per cycle, so quotes never mix two versions. Nodes poll or subscribe for the new version.
surge_configPostgrescityvehicle_typecurve parametersmax_multipliermin_stepsmoothing paramsversionVersioned config; each snapshot records the config version used.
overridesPostgres + snapshotoverride_idpolygon (H3 cell set)vehicle_typemode (cap, floor, fixed, off)starts_atends_atcreated_byapproved_byApplied as the last step of the pricing job.
surge_historyClickHouse / S3 Parquettscitycellvehicle_typedemandsupplyraw_multiplierfinal_multiplierconfig_versionsnapshot_versionFor disputes ('why was it 1.8x at 9:02?'), research and model training.
quotesKafka → S3, Redis for active quotesquote_idrider_idcellmultipliersnapshot_versionfareexpires_atconvertedConversion rate per multiplier is the key signal for tuning (price elasticity).
5 Architecture
Hover a box to highlight its connections.
6 Request flows
Compute surge for a city
- 1Driver pings and rider events flow into Kafka, partitioned by city.
- 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.
- 3Every 30-60 s, the per-city pricing job reads the latest counts for all cells in the city.
- 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).
- 5It smooths: spatially (blend with neighbouring cells' ratios using H3 rings) and over time (exponential moving average, max step up or down per cycle).
- 6It applies rules in order: round to 0.1 steps, city and regulatory caps, active overrides, and the kill switch.
- 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
- 1Rider at a pickup point asks for fares; the quote service calls Surge.GetMultiplier for each vehicle type.
- 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.
- 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.
- 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
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.