System Design: Design a Ride-Sharing App (Uber/Lyft)
Designing Uber or Lyft is one of the most popular system design interview questions. It combines real-time geospatial queries, matching algorithms, dynamic pricing, and high-throughput event processing.
Requirements
Functional: rider requests a ride with pickup/dropoff, system matches a nearby driver, real-time tracking during the trip, dynamic surge pricing, trip history and payments.
Non-functional: match within 30 seconds, handle 1M+ concurrent users, location updates every 4 seconds per driver, sub-second driver lookup within radius, global availability.
Key Components
Rider App Driver App
│ request ride │ location ping (every 4s)
▼ ▼
┌────────────────────────────────────────────────┐
│ API Gateway / Load Balancer │
└──────────────────┬─────────────────────────────┘
│
┌─────────────┼─────────────┐
▼ ▼ ▼
Location Matching Trip
Service Service Service
(geospatial) (dispatcher) (state machine)
│ │ │
▼ ▼ ▼
Redis Geo PostgreSQL Kafka
(driver pos) (trips DB) (events)
Location Service — Driver Position Tracking
Drivers send GPS updates every 4 seconds. At 1M active drivers: 250K location updates/second.
- Redis GEO commands: GEOADD stores (longitude, latitude, member) as a sorted set with geohash scores. GEORADIUS returns all members within a radius in O(N+log(M)) — perfect for finding nearby drivers.
# Driver sends location update
redis.geoadd("drivers:active", longitude, latitude, driver_id)
redis.expire(f"driver:{driver_id}:location", 30) # expire if driver goes offline
# Find drivers within 2km of pickup
nearby = redis.georadius(
"drivers:active",
pickup_lng, pickup_lat,
radius=2, unit="km",
withcoord=True,
withdist=True,
count=50, # limit candidates
sort="ASC" # nearest first
)
Scaling: shard the drivers sorted set by geohash prefix (city or region). Drivers in NYC update drivers:active:nyc, drivers in LA update drivers:active:la.
Matching Service (Dispatcher)
- Rider requests ride with pickup + dropoff + ride_type
- Query Location Service for N nearest available drivers (radius 2km, expand to 5km if none)
- Filter by: vehicle type, driver rating, acceptance rate
- Rank by: ETA (estimated time of arrival) — requires road network routing
- Send offer to best driver (1 driver at a time or top-3 simultaneously)
- If driver accepts within 15s → assign; if declines or times out → next driver
- Timeout after 3 rounds → notify rider no drivers available
ETA Calculation
- Simple: straight-line distance / average speed × traffic factor. Fast, ~500ms.
- Accurate: road network graph (Dijkstra / A*) with real-time traffic weights. Uber uses a custom routing engine (H3 hexagonal grid). Slower but precise.
- Pre-compute ETAs for driver-to-pickup pairs in batch; serve from cache for common pairs
Trip State Machine
States:
REQUESTED → MATCHED → DRIVER_EN_ROUTE → ARRIVED → IN_TRIP → COMPLETED
└→ CANCELLED
Transitions stored in Kafka (event log) + PostgreSQL (current state)
Each state change emits an event to notify rider and driver apps via WebSocket/push
Dynamic Surge Pricing
- Compute demand/supply ratio per geohash cell every 1 minute
- surge_multiplier = f(demand / supply) — Uber uses 1.1x, 1.5x, 2x, 3x tiers
- Display surge estimate before booking; require rider to confirm surge rate
- Store surge history for analytics and model training
Payment Flow
- Upfront price estimate: base fare + per-mile + per-minute + surge + booking fee
- Final charge at trip end with actual distance/duration
- Payment processor: Stripe or Braintree; supports multiple methods (card, wallet, cash)
- Driver payout: weekly batch settlement, instant payout for premium drivers
Interview Checklist
- Draw: Location Service (Redis GEO) → Matching (dispatcher) → Trip (state machine)
- Explain Redis GEOADD/GEORADIUS for O(1) driver storage, O(log n + k) radius query
- Describe the matching loop: offer → 15s timer → next driver
- Cover surge pricing: demand/supply ratio per geohash cell
- Mention scalability: shard drivers by city, use WebSocket for real-time tracking
{
“@context”: “https://schema.org”,
“@type”: “FAQPage”,
“mainEntity”: [
{
“@type”: “Question”,
“name”: “How does Uber use Redis for real-time driver location tracking?”,
“acceptedAnswer”: { “@type”: “Answer”, “text”: “Uber stores driver locations using Redis GEO commands: GEOADD adds a driver's (longitude, latitude) keyed by driver_id to a geospatial sorted set. The score is a 52-bit geohash. GEORADIUS then returns all drivers within a specified radius in O(N+log M) time. At 1M active drivers sending updates every 4 seconds, this is ~250K writes/second — Redis handles this with sub-millisecond latency. Regions are sharded by city to distribute load.” }
},
{
“@type”: “Question”,
“name”: “How does a ride-sharing app match riders to drivers?”,
“acceptedAnswer”: { “@type”: “Answer”, “text”: “The dispatcher: (1) queries the Location Service for nearby available drivers (GEORADIUS within 2km, expand if none), (2) filters by vehicle type, rating, and acceptance rate, (3) ranks remaining candidates by ETA to pickup using a routing engine, (4) sends a trip offer to the best driver with a 15-second accept window, (5) if declined or timed out, offers to the next candidate. This continues for up to 3 rounds before returning "no drivers available" to the rider.” }
},
{
“@type”: “Question”,
“name”: “How does surge pricing work in a ride-sharing system?”,
“acceptedAnswer”: { “@type”: “Answer”, “text”: “Every minute, the pricing service computes the demand/supply ratio for each geohash cell (roughly 1km²). Demand = active ride requests; Supply = available drivers. When demand/supply exceeds a threshold, a surge multiplier (1.1x, 1.5x, 2x, 3x) is applied to the base fare. The multiplier is shown to riders before booking with explicit confirmation required for high surge. Historical surge data feeds ML models that predict future demand for pre-positioning drivers.” }
}
]
}