Ride-sharing services like Uber and Lyft are canonical low level design problems because they combine real-time geospatial data, distributed state machines, dynamic pricing, and high-throughput event pipelines. This post walks through the core subsystems you need to design and the key decisions at each layer.
Trip State Machine
A trip’s lifecycle is best modeled as a finite state machine. The states are: requested → driver_assigned → driver_arriving → trip_started → trip_completed → payment_collected. Each transition is an explicit event with a timestamp, actor, and metadata stored in the trip_events table.
Every state transition publishes a Kafka event to the trip-events topic. Downstream consumers include:
- Notification service: sends push notification to driver on assignment, to passenger on driver arrival, and receipt on payment collection.
- Billing service: listens for
trip_completedto calculate fare andpayment_collectedto finalize accounting. - Analytics service: streams all events into the data warehouse for demand forecasting and driver behavior analysis.
The state machine enforces valid transitions server-side. An attempt to move from requested directly to trip_started is rejected. The trips table holds current state with an optimistic lock version column to prevent concurrent update races.
Geospatial Driver Search
Driver locations are stored in Redis using the GEOADD command with key drivers:available. Each driver updates their position every 3-5 seconds via a WebSocket connection maintained by the driver app. The update rate is adaptive: when stationary the interval extends to 10 seconds to reduce load.
When a passenger requests a ride, the matching service calls GEORADIUS drivers:available {lng} {lat} 5 km ASC COUNT 20 to retrieve the nearest 20 available drivers. The initial radius is 2 km, expanding to 5 km and then 10 km if insufficient drivers are found.
Drivers are then ranked by ETA to pickup, not raw distance. A driver 1.2 km away on the other side of a river may have a higher ETA than one 1.8 km away on the same road. ETA computation is described in its own section below. The Redis geo index is updated atomically; a driver who accepts a trip is removed from drivers:available and added to drivers:on_trip.
Driver-Passenger Matching
Matching uses a sequential offer model rather than broadcasting to all candidates simultaneously. The process:
- Select the top-ranked available driver by ETA.
- Send a push notification and WebSocket event with trip details.
- Open a 10-second accept window. The driver sees a countdown timer.
- If the driver accepts, the trip transitions to
driver_assigned. - If the driver declines or the window expires, move to the next candidate.
If the entire initial candidate list is exhausted without acceptance, the system applies exponential backoff on the search radius: retry with radius * 1.5, up to a maximum configured radius. After a configurable timeout (typically 3 minutes) the request is failed with a "no drivers available" response.
Driver acceptance rate is tracked per driver. Drivers with low acceptance rates are deprioritized in future matching, as high refusal rates degrade passenger experience.
Surge Pricing
The city is partitioned into geohash cells at precision level 5 (approximately 4.9 km x 4.9 km). For each cell, the system maintains a rolling 5-minute count of open ride requests (demand) and a count of available drivers (supply).
The surge multiplier formula is:
multiplier = max(1.0, (demand / supply) * base_factor)
Where base_factor is tuned per market (typically 0.5-0.8). The multiplier is capped at a regulatory or business maximum (e.g., 3.0x during normal operations, higher during declared emergencies only with approval).
To avoid rapid oscillation, the multiplier is smoothed using exponential moving average: new_multiplier = alpha * raw_multiplier + (1 - alpha) * previous_multiplier with alpha = 0.3. This prevents the surge display from flickering for passengers refreshing the app.
The surge multiplier is shown to the passenger on the fare estimate screen and must be explicitly acknowledged before booking proceeds. The acknowledged multiplier is locked to the trip; if surge increases after booking the passenger pays the locked rate.
ETA Computation
ETA has two components: ETA to pickup and ETA to destination.
ETA to pickup is computed by calling a routing API (internal or third-party like Google Maps Routes API) with the driver’s current GPS coordinates as origin and the passenger pickup point as destination. The routing API accounts for current traffic conditions. This call is made for the top N driver candidates in parallel to avoid sequential latency.
ETA to destination uses a combination of:
- Routing API for the base road-network time.
- Historical trip time lookup: for the same origin-destination geohash pair at the same hour of day and day of week, the p50 of completed trip durations is used as a correction factor.
- An ML model trained on trip features (origin, destination, time, weather, events) that outputs a refined ETA with a confidence interval.
ETAs are recomputed every 60 seconds during the trip and pushed to the passenger app via WebSocket.
Route and Navigation
Once a trip is matched, the routing service generates a polyline from the driver’s current location through the pickup point to the destination. This polyline is sent to the driver app as an encoded Google Polyline string.
During the trip, the driver app streams GPS positions. The backend compares the driver’s position against the planned route. If the driver deviates by more than 150 meters for more than 30 seconds, a re-route is triggered automatically. The new route is computed and pushed to the driver app.
Toll detection is performed by checking whether the route geometry intersects with stored toll zone polygons. Detected tolls are added to the fare breakdown. Turn-by-turn instructions are generated from the route and delivered to the driver app with bearing angles for arrow display.
Safety Features
Safety is a first-class concern in ride-sharing design:
- Trip sharing: passengers can share a live tracking link with a contact. The link serves a read-only map view without requiring app installation, authenticated via a short-lived signed token.
- Emergency button: in-app SOS sends the passenger’s GPS, trip ID, driver ID, and timestamp to a safety operations center and optionally dials emergency services.
- Route deviation detection: the backend monitors for significant off-route travel toward no destination (not just re-routing). Anomalous deviation patterns trigger an automated check-in notification to the passenger.
- Identity verification: drivers undergo background checks before activation. Passengers verify phone number at registration. License plate and driver photo shown to passenger before pickup.
Driver Rating System
After trip completion, the passenger is prompted to rate the driver on a 1-5 scale with optional text feedback. The rating prompt appears after the payment receipt screen with a timeout; unrated trips default to neutral (not counted).
Driver ratings are stored in a driver_ratings table with trip_id, passenger_id, driver_id, score, and timestamp. A rolling 30-day average is maintained in the driver_stats table and recomputed on each new rating via a background job.
Thresholds:
- Below 4.6: driver receives in-app coaching tips and quality resources.
- Below 4.3 sustained over 100+ trips: account flagged for review.
- Below 4.0 hard limit: driver account deactivated pending appeal.
Ratings are also used as a soft signal in driver matching: passengers who consistently rate poorly may be deprioritized by high-rated drivers in markets where driver choice is enabled.
See also: Uber Interview Guide 2026: Dispatch Systems, Geospatial Algorithms, and Marketplace Engineering
See also: Lyft Interview Guide 2026: Rideshare Engineering, Real-Time Dispatch, and Safety Systems