What Is a Ride-Sharing System?
A ride-sharing platform matches riders requesting trips with nearby available drivers in real time, computes dynamic pricing, and tracks trips from request to completion. Examples: Uber, Lyft, DiDi. Core challenges: sub-second driver matching across millions of concurrent location updates, surge pricing computation, and ETA accuracy under traffic variability.
System Requirements
Functional
- Rider requests a trip with pickup and destination
- System finds and matches the nearest available driver
- Driver accepts or declines; if decline, offer to next driver
- Real-time location tracking of driver en route
- Dynamic (surge) pricing based on supply/demand
- Trip completion and payment processing
Non-Functional
- 5M drivers, 10M riders, 1M concurrent active trips
- Driver location updates: every 4 seconds = 1.25M updates/sec
- Match latency: <3 seconds from trip request to driver offer
Architecture Overview
Driver App ──GPS update──► Location Service ──► Redis GeoSet
Rider App ──request──► Matching Service ──► Supply Service
│
ETA Service (routing)
Pricing Service (surge)
Payment Service (on completion)
Location Service
Driver app sends GPS coordinates every 4 seconds. Location service updates Redis GeoSet:
GEOADD drivers_available lng lat driver_id
# On driver going offline or accepting trip:
ZREM drivers_available driver_id
Maintain separate GeoSets: drivers_available (accepting requests), drivers_on_trip. Sharding: partition by geohash cell across Redis nodes. 1.25M updates/sec across ~50 Redis shards = 25K ops/sec per shard — manageable.
Matching Service
def match_driver(pickup_lat, pickup_lon, rider_prefs):
# 1. Find candidates within radius
candidates = redis.georadius(
'drivers_available', pickup_lon, pickup_lat,
radius=5, unit='km', sort='ASC', count=10
)
if not candidates:
expand radius to 10km and retry
# 2. Score candidates
for driver in candidates:
score = (
0.5 * proximity_score(driver, pickup) +
0.3 * driver_rating_score(driver) +
0.2 * acceptance_rate_score(driver)
)
# 3. Offer to top-ranked driver, wait 15s for acceptance
best_driver = max(candidates, key=score)
offer_trip(best_driver, trip_id)
If the driver declines or does not respond within 15 seconds, offer to the next candidate. Track which drivers have been offered this trip (avoid re-offering).
Trip State Machine
REQUESTING ──match found──► DRIVER_ASSIGNED
│
no drivers found ──► NO_DRIVERS (retry)
DRIVER_ASSIGNED ──driver accepts──► EN_ROUTE_TO_PICKUP
EN_ROUTE_TO_PICKUP ──driver arrives──► WAITING_FOR_RIDER
WAITING_FOR_RIDER ──rider boards──► IN_PROGRESS
IN_PROGRESS ──destination reached──► COMPLETED
Any state ──cancel──► CANCELLED
Surge Pricing
Surge multiplier is computed per geographic zone (geohash cell, ~1km square). Every 30 seconds: query driver count and trip request count per zone. Compute supply/demand ratio. Apply surge if demand significantly exceeds supply:
surge_multiplier = max(1.0, sqrt(demand_rate / supply_rate))
Cap at 5x. Display clearly to rider before they confirm. Store surge_multiplier with the trip record for transparency and dispute resolution.
ETA Computation
ETA = routing_time(driver_location → pickup) + routing_time(pickup → destination). Routing: use pre-computed road graph with time-of-day weighted edges (traffic patterns). Update driver-to-pickup ETA every 30 seconds as driver moves. Destination ETA shown to rider is an estimate — recompute at pickup.
Payment
At trip completion: compute final fare = base_fare + (per_minute * duration) + (per_km * distance) * surge_multiplier. Charge the rider’s stored payment method via Stripe (with an idempotency key = trip_id). Transfer driver payout (net of platform fee) on a weekly or daily schedule to their bank account.
Interview Tips
- Redis GeoSet + GEORADIUS is the canonical answer for nearby driver search.
- Draw the trip state machine before designing any DB schema.
- Surge pricing = supply/demand ratio per geohash zone, recomputed every 30 seconds.
- Driver offer with 15-second timeout and cascade to next candidate shows real-world awareness.
{
“@context”: “https://schema.org”,
“@type”: “FAQPage”,
“mainEntity”: [
{
“@type”: “Question”,
“name”: “How does a ride-sharing platform find the nearest available drivers in under 3 seconds?”,
“acceptedAnswer”: { “@type”: “Answer”, “text”: “Nearest driver search uses Redis GeoSet with GEORADIUS. Every active driver sends a GPS update every 4 seconds; the location service updates their position: GEOADD drivers_available longitude latitude driver_id. GEORADIUS drivers_available pickup_lon pickup_lat 5 km ASC COUNT 10 returns up to 10 nearest drivers sorted by distance in O(log N + K) time. For 5M drivers across all markets, shard by geohash prefix across ~50 Redis nodes — each shard handles one geographic region. GEORADIUS for a 5km radius in one city queries a single shard containing ~50K drivers in that city, returning in under 5ms. Total request path: rider request → matching service → Redis GEORADIUS → score candidates → offer to top driver: under 200ms. The 3-second SLA includes the driver app receiving and displaying the offer (network RTT) and the driver having time to accept.” }
},
{
“@type”: “Question”,
“name”: “How does surge pricing work and how do you compute it efficiently?”,
“acceptedAnswer”: { “@type”: “Answer”, “text”: “Surge pricing adjusts ride costs when demand exceeds supply in a geographic area. Implementation: divide the service area into geohash cells (~1km x 1km squares at precision 6). Every 30 seconds, a pricing service queries Redis for: (1) active driver count per cell (drivers_available GeoSet members in the cell), (2) trip request rate per cell (from a sliding window counter in Redis). Surge multiplier = max(1.0, sqrt(demand_rate / supply_rate)). Cap at 5x. Store results in a Redis hash: surge:{geohash_cell} → multiplier with 60-second TTL. At trip request time: determine the pickup geohash cell, look up the surge multiplier in O(1). Compute the fare estimate: base_fare * surge_multiplier. Display prominently to the rider before they confirm. Store the surge_multiplier in the bookings table for auditing — riders who were charged surge can dispute if it was applied incorrectly. The 30-second recompute cadence balances responsiveness with compute cost.” }
},
{
“@type”: “Question”,
“name”: “How do you handle the driver offer cascade when a driver declines a trip?”,
“acceptedAnswer”: { “@type”: “Answer”, “text”: “When a trip request arrives, the matching service ranks nearby drivers and offers to the top-ranked candidate. The driver has a 15-second acceptance window (configurable). If they decline or do not respond: mark this driver as "offered this trip" (store in Redis set: offered:{trip_id} → {driver_id} with TTL matching the trip lifetime). Offer to the second-ranked driver. Repeat up to a configurable max (e.g., 5 drivers). After 5 refusals: expand the search radius and repeat. After the radius reaches a maximum (e.g., 20km) or a total timeout (e.g., 90 seconds): return "no drivers available" to the rider. Driver state transitions on each offer: move from drivers_available to a "considering offer" state; move back to available if they decline or timeout. This prevents the same driver from receiving multiple concurrent offers. Drivers who frequently decline have their acceptance_rate tracked — it factors into the matching score, gradually offering trips to more reliable drivers first.” }
}
]
}