Loading…
Loading…
Design a ride-sharing platform like Uber. Riders request rides, drivers accept, and the system matches them in real time. Track driver location, display ETA, and handle surge pricing.
[Driver App]
→ location update every 5s
→ [Location Service]
→ Redis Geo (GeoHash index)
→ Kafka (location events for analytics)
[Rider App]
→ ride request
→ [Matching Service]
→ queries Redis Geo for nearby drivers
→ sends offer to selected driver
→ [Driver App] accepts
[Trip Service]
→ manages active trip state
→ streams location to rider via WebSocket
[Pricing Service]
→ calculates surge multiplier from supply/demand ratio
→ feeds pricing to Matching Service
The architecture above uses Redis GEO (geohash-based) — worth naming the alternative explicitly. A quadtree recursively subdivides 2D space into four quadrants, going deeper only where point density is high, which adapts naturally to uneven driver distribution (dense in a city center, sparse in a suburb) without manual tuning. Geohashing (fixed-precision string encoding of lat/lng into a sorted key) is simpler to implement on top of an existing key-value store (exactly why Redis GEO works the way it does) and enables straightforward proximity queries via key-prefix matching, but its FIXED grid size doesn't adapt to density the way a quadtree does — a geohash cell sized well for a dense city center is often far too coarse or too fine elsewhere. Redis GEO's real-world convenience (a production-ready primitive requiring no custom index to build/maintain) usually outweighs a quadtree's better density-adaptivity for most ride-sharing scale.
The Trip Service above streams location via WebSocket specifically because ride tracking is latency-sensitive and genuinely bidirectional-feeling (the rider expects near-instant position updates without requesting them). Polling (the rider's app periodically requesting the driver's current position) would work but adds latency (bounded by the poll interval) and wasted requests when nothing has changed — the same WebSocket-vs-polling tradeoff covered generally in Design WhatsApp's real-time delivery discussion applies directly here.
// Driver
PUT /drivers/{driverId}/location
Body: { lat, lng, heading, speed }
PATCH /drivers/{driverId}/status { status: AVAILABLE | ON_TRIP | OFFLINE }
// Rider
POST /rides/request
Body: { riderId, pickup: {lat,lng}, destination: {lat,lng} }
Response: { rideId, driverEta, estimatedFare }
GET /rides/{rideId}/driver-location → real-time via WebSocket
// Matching
GET /drivers/nearby?lat=X&lng=Y&radius=2km&limit=10
Driver locations (Redis GEO — O(log N) geospatial queries)
GEOADD drivers:available <lng> <lat> "driver:42"
GEOSEARCH drivers:available FROMMEMBER <point> BYRADIUS 2 km ASC COUNT 10
Trips (PostgreSQL — ACID, payment reconciliation)
rides
id UUID PK
rider_id UUID
driver_id UUID
pickup POINT (PostGIS geometry)
destination POINT
status ENUM (REQUESTED, ACCEPTED, ONGOING, COMPLETED, CANCELLED)
started_at TIMESTAMP
ended_at TIMESTAMP
fare DECIMAL
Trip tracking (Cassandra — time-series location history)
trip_location
trip_id UUID PK
recorded_at TIMEUUID
lat, lng DOUBLE
Redis GEO uses GeoHash under the hood — O(log N) radius search. With 1M active drivers:
GEOSEARCH radius 2km returns results in <1mssurge_multiplier = demand / supply
where demand = ride_requests_last_10min in zone
supply = available_drivers_last_10min in zone
if surge > 1.5: show surge warning to rider
The scaling section above describes nearest-available-driver matching — simple, minimizes rider wait time for that specific request. The alternative worth naming: balancing driver utilization across a zone, deliberately NOT always picking the geometrically nearest driver, instead factoring in each nearby driver's recent idle time or earnings — this improves fairness and overall driver retention/satisfaction across the whole platform, at the cost of occasionally giving an individual rider a slightly longer wait than pure nearest-driver would. Production ride-sharing systems generally blend both: nearest-driver as the primary signal, with utilization as a secondary tie-breaker among close candidates, rather than purely optimizing one metric.
Mobile network instability means the Trip Service can't assume a continuous connection — a driver's app losing connectivity mid-trip shouldn't corrupt ride state or leave the rider unable to see any updates. The standard approach: the Trip Service treats itself as the single source of truth for ride state (not the client apps), with each app reconnecting and re-syncing full current state on reconnect rather than relying on a continuous stream of incremental updates it might have partially missed. A brief location staleness during a disconnect is an acceptable, expected condition (shown as 'reconnecting' in the rider UI) rather than a failure requiring special-case handling — the system is explicitly designed to tolerate transient disconnects as normal operation, not an edge case.
Q: How are driver locations stored so "nearby drivers" queries are fast? A: Drivers send location updates every few seconds. The latest position goes into an in-memory geospatial index (Redis GEO, geohash buckets, or a quadtree or H3 cells), keyed by cell. A nearby search looks up the rider's cell and its neighbours, rather than scanning all drivers. Only the latest position is needed for matching, and the full history goes to a separate, asynchronous pipeline.
Q: How do you prevent two riders from being matched to the same driver? A: Reserve the driver atomically before sending the offer, with a conditional update or a short-lived lock on the driver's state (AVAILABLE → OFFERED). A second matcher that picks the same driver fails the reservation, and moves on to its next candidate. If the driver declines or times out, the reservation is released.
Q: How does surge pricing work at the system level? A: For each area cell, a stream processor compares recent ride requests (demand) with available drivers (supply) over short windows, and computes a multiplier with smoothing, so prices don't flicker. The multiplier is attached to fare quotes, and the quote is locked for the rider for a short period, so the price they accepted is the price they pay.
Q: How is the trip's fare calculated reliably if the app loses connectivity? A: The driver app records GPS points locally and uploads them in batches when connected. The server reconstructs the route (map-matching the points to roads) and computes distance and time from the server-side data, rather than trusting totals computed by the client. That makes fares consistent and resistant to tampering.
Q: How would you scale the system across cities? A: Partition by geography. Each city or region is served by its own set of matching and location services, since trips almost never cross regions. That keeps data local, limits the blast radius of failures, and lets each region scale to its own demand. Global services (accounts, payments) sit above the regional ones.