Surge Pricing
Design a dynamic pricing system for a ride-sharing platform that detects demand-supply imbalances in real time, computes a surge multiplier, and integrates pricing seamlessly into the matching and booking flows.
What is a surge pricing system?
A surge pricing system detects when ride demand outpaces driver supply in a geographic area, computes a price multiplier, and applies it to new trips in real time. The engineering challenge is not the math; it is doing geo-cell aggregation at sub-30-second freshness across millions of concurrent requests without the multiplier oscillating every few seconds and eroding rider trust. The design tests real-time stream processing, geospatial partitioning, cache design, and feedback-loop control in one question.
TL;DR
Partition geography into cells, publish ride-request and driver-status events to Kafka keyed by cell_id, and let a stateful Aggregator Worker maintain demand/supply counts. Smooth the ratio with EWMA and hysteresis, then push the current multiplier to Redis every 30 seconds. The Booking Service reads Redis directly on the pricing path; the Pricing Service is a read API for clients and analytics. If the pricing dependency is unavailable, fail open to a multiplier of 1.0 and record the degradation.
Scope and assumptions
- The baseline covers cell-level surge for one region or city, with a single logical stream-processing topology and region-local Redis reads.
- Demand means pending ride requests; supply means available drivers. The exact matching algorithm is outside this design but emits the status events used by the aggregator.
- A multiplier may be up to 30 seconds old, must become visible within about 2 seconds of a meaningful change, and is allowed to move in configured increments.
- The booking path needs a fast, available read; multiplier computation is asynchronous and does not run synchronously for every rider request.
- Per-rider personalization, long-term forecasting, incentive programs, fraud analytics, and cross-city/global normalization remain separate extensions.
Functional Requirements
Core Requirements
- Detect when demand (ride requests) exceeds supply (available drivers) in a geographic area in near real time.
- Compute a surge multiplier for that area and apply it to new trip prices.
- Show riders the current surge multiplier before they confirm a booking.
- Automatically remove surge when supply and demand rebalance.
Scope Exclusions
- Driver incentive programs for surges.
- Long-term price forecasting.
The hardest problem in scope: Preventing surge oscillation. A pure real-time system where any demand spike triggers a multiplier increase and any supply recovery immediately drops it back to 1.0 creates a sawtooth wave that confuses riders and drivers alike. The oscillation deep dive is the highest-leverage stability discussion in this design.
Driver incentive programs are below the line because they run after a surge is detected, not during the detection or computation path. To add them: publish a SurgeActivated event to a Kafka topic; a separate Incentive Service subscribes, evaluates driver eligibility, and sends push notifications. It sits beside the Pricing Service, not inside it.
Long-term price forecasting is below the line because it relies on historical data warehousing and model training infrastructure that does not touch the real-time multiplier path. To add it: feed SurgeEvent records from the audit log into an offline ML pipeline that predicts surge windows by time of day and area and pre-publishes expected multipliers via a separate ForecastedSurge table.
Non-Functional Requirements
Core Requirements
- Multiplier latency: The surge multiplier is visible to a rider within 2 seconds of a demand spike in that cell.
- Freshness: Multipliers are recomputed every 30 seconds per geo-cell. A 30-second-old multiplier is stale but acceptable; a 5-minute-old one causes mis-pricing complaints.
- Read throughput: The Pricing Service handles multiplier lookups at the rate of the booking request volume, roughly 50K requests per second at peak globally.
- Availability: 99.99% uptime. Fail open: if the Pricing Service is unreachable, the Booking Service uses a multiplier of 1.0 rather than blocking the booking.
- Scale: 5M active drivers globally, 10M concurrent riders, and thousands of geo-cells active simultaneously.
- Consistency: Eventual consistency for multiplier reads is acceptable. A rider seeing a multiplier that is 30 seconds stale is a minor UX issue; an unavailable pricing endpoint is a revenue-stopping outage.
Below the Line
- Per-rider personalized pricing
- Cross-city multiplier normalization
- Real-time audit fraud detection on multiplier values
The hardest architectural constraint: 50K multiplier lookups per second with a 2-second visibility SLA. This eliminates any design where the Booking Service queries a database directly on every request. The multiplier must live in an in-process or near-process cache that is refreshed by an independently operating computation pipeline.
Per-rider personalized pricing is below the line because it requires user-level demand modeling that adds significant latency to the lookup path and changes the Pricing Service from a cell-keyed read to a user-keyed computation. To add it: run a separate Personalization Service in parallel and merge its multiplier adjustment with the cell-level multiplier before returning the final price.
Cross-city multiplier normalization is below the line because surge in one city does not propagate to another. Each city's cells are independent. Global multiplier coordination would require a consensus layer that adds latency without a clear user benefit.
30-second answer
Use H3-style geo-cells and Kafka topics partitioned by cell_id. The Aggregator Worker consumes ride requests and driver status changes for each cell, keeps local counts, applies EWMA smoothing plus hysteresis, and writes the multiplier to Redis every 30 seconds. The Booking Service reads Redis directly and applies the value to the fare; a Pricing API can expose the same cached value. If the pricing dependency is unavailable, fail open to 1.0 and record the degradation.
5-minute explanation
- Set the contract. Define cell-level surge, the 30-second recompute interval, two-second visibility goal, and fail-open behavior. Clarify that this is traffic-sensitive pricing, not personalized or predictive pricing.
- Explain the signal. Ride requests increase demand; driver availability changes supply. Both events carry
cell_idand an event ID, and Kafka partitions preserve per-cell ordering for one aggregator owner. - Move computation off the hot path. The aggregator keeps state, calculates a smoothed demand/supply ratio, applies activation/deactivation thresholds, and pushes a small value to Redis. Booking reads one key instead of scanning events or calling a database.
- Walk the critical flow. A rider requests a fare; Booking reads
surge:cell:{id}and applies the multiplier. A driver status or ride-request event changes counts; the aggregator updates Redis and writes an audit event only when the multiplier changes materially. - Address control and failure. EWMA, hysteresis, consecutive-cycle confirmation, cell-neighbor expansion, event replay, Redis TTLs, and a default multiplier prevent oscillation, stale pricing, and total booking blockage.
45-minute interview approach
- 0β3 min β Clarify scope. Confirm the geography, cell resolution, meaning of demand and supply, freshness target, price-lock semantics, and whether personalized pricing or incentives are included.
- 3β8 min β Establish the numbers. Estimate active cells, ride-request and driver-event rates, peak price reads, event lag tolerance, Redis capacity, and the acceptable multiplier staleness.
- 8β13 min β Define entities and APIs. Sketch
GeoCell,RideRequest,Driver, andSurgeEvent; show the read API and the event contracts. Makecell_id, event time, and idempotency fields explicit. - 13β22 min β Draw the baseline. Show producers, Kafka topics, per-cell Aggregator Workers, Redis, Booking Service, Pricing Service, and the audit store. Walk from an event to a cached multiplier to a fare.
- 22β35 min β Prioritize the hard parts. Spend most of the time on partitioning by cell, EWMA/hysteresis and the zero-supply case, late/duplicate events, cell-boundary effects, and why Booking reads Redis directly instead of invoking a compute service.
- 35β40 min β Cover reliability, security, and operations. Discuss consumer lag, replay/checkpoints, Redis TTL and fail-open behavior, authenticated service-to-service writes, pricing integrity, and per-cell monitoring.
- 40β44 min β Compare alternatives. Contrast database polling with streaming, fixed thresholds with smoothing, fine versus coarse cells, and synchronous global coordination with region-local eventual consistency.
- 44β45 min β Recap and invite follow-ups. Restate the event-to-Redis pipeline, the stability controls, and the fare behavior when data is stale or unavailable.
Core Entities
- GeoCell: A geographic unit (identified by
cell_id) with the currentdemand_count,supply_count,multiplier, andcomputed_attimestamp. The primary keyed object the Pricing Service reads and writes. - SurgeEvent: An immutable audit record written whenever a multiplier changes. Contains
cell_id,old_multiplier,new_multiplier,demand_count,supply_count, andevent_time. Feeds ML training and compliance reporting. - RideRequest: A demand signal. Carries
request_id,rider_id,pickup_cell_id,status, andcreated_at. The aggregator counts pending requests per cell to compute demand. - Driver: A supply signal. Carries
driver_id,status(available, on_trip, offline), andcurrent_cell_id. The aggregator counts available drivers per cell to compute supply.
Full schema, partition keys, and indexes are deferred to the deep dives. These four entities are sufficient for the API design and High-Level Design.
API Design
FR 1 and FR 3 - Rider sees surge multiplier before booking:
GET /pricing/surge?cell_id={cell_id}
Response: { cell_id, multiplier, computed_at }
This is the hot read path. The Booking Service calls this endpoint on every ride request before presenting the final price to the rider. The response must be fast (under 10ms) because it sits in the critical path of booking. The computed_at field allows the Booking Service to surface "prices updated 15s ago" messaging in the UI.
FR 2 - Internal: Aggregator publishes demand and supply counts:
The aggregator does not expose an HTTP endpoint. It reads from two Kafka topics and writes computed cell states to Redis.
// Kafka topic: ride.requests
// Published by Booking Service on every ride request
{ request_id, pickup_cell_id, event_time }
// Kafka topic: driver.status
// Published by Location Service on every driver availability change
{ driver_id, cell_id, status, event_time }
FR 2 - Internal: Pricing Service computes and stores multiplier:
POST /pricing/compute (internal, called by Aggregator Worker)
Body: { cell_id, demand_count, supply_count }
Response: { cell_id, multiplier }
In the evolved design this becomes a Redis write directly from the Aggregator Worker rather than an HTTP call; the HTTP shape is shown here to make the contract explicit before the architecture evolves.
FR 4 - Surge removal is implicit: The Aggregator recomputes every cell on a 30-second schedule. When demand_count / supply_count falls below the deactivation threshold, the multiplier resets to 1.0 and a SurgeEvent is written with new_multiplier = 1.0. No separate "remove surge" API is needed.
High-Level Design
1. Detecting demand-supply imbalance
The system must count pending ride requests and available drivers per geo-cell, then compare them. The naive approach polls a database on a timer.
Components:
- Booking Service: Records every ride request and publishes a demand event.
- Location Service: Tracks driver availability and publishes supply events.
- Aggregator: A scheduled job that queries the primary DB for demand and supply counts per cell every 60 seconds.
- Surge DB: Stores
GeoCellrecords with demand, supply, and multiplier.
Request walkthrough:
- Rider submits a ride request; Booking Service inserts it into Surge DB with status
pending. - Driver sends a GPS update; Location Service updates driver status in Surge DB.
- Aggregator runs every 60 seconds, executes
SELECT cell_id, COUNT(*) FROM ride_requests GROUP BY pickup_cell_idandSELECT cell_id, COUNT(*) FROM drivers WHERE status='available' GROUP BY current_cell_id. - Aggregator writes updated
demand_countandsupply_countinto eachGeoCellrow. - Pricing Service reads the
GeoCelltable when the Booking Service queries for the multiplier.
This covers demand detection and multiplier reads. The 60-second poll interval already violates the 2-second freshness NFR. The next section evolves to streaming aggregation.
2. Real-time aggregation with streaming events
A 60-second cron job cannot meet a 2-second freshness SLA. The fix is replacing the batch poll with a streaming event pipeline that maintains rolling counts in memory.
Start with the naive cron-poll version and its two failure modes before introducing Kafka: slow full-table scans and a response delay equal to the polling window. This makes the streaming choice follow directly from the freshness target.
The DB-poll approach has two failure modes. First, a SELECT COUNT GROUP BY cell across millions of ride requests and drivers runs a full table scan; at peak it adds seconds of query time before any multiplier update is written. Second, the 60-second window means a demand spike from a concert ending triggers no surge response for up to a minute, during which all riders see a 1.0 multiplier and the system fails to clear the queue.
Continue Reading with Premium
Unlock this article and every other in-depth system design guide on the platform with SDEpedia Premium.
Related Articles
Design the driver dispatch engine that matches a rider's request to the nearest available driver in milliseconds, covering real-time geospatial indexing, conflict prevention, and the rebalancing challenges of a global fleet.
Walk through a complete Uber design, from a single trip service to a globally distributed system handling 5M concurrent drivers, real-time GPS matching, and sub-5-second dispatch.
Design an ETA service for a ride-sharing app that computes accurate travel time estimates in real time, using Contraction Hierarchies, a GPS probe pipeline, and SSE push updates at 100M requests per minute.