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.