🚗 System Design: Uber / Ride-Sharing
Location-based, real-time matching at global scale.
Step 1: Requirements
Functional
- Rider requests a ride
- Match rider with nearby driver
- Real-time location tracking of driver
- Dynamic pricing (surge pricing)
- ETA calculation
- Route navigation
Non-Functional
- 10M trips/day, 5M active drivers
- Location updates every 4 seconds
- Match should happen in < 3 seconds
- 99.99% uptime
Step 2: Capacity
Location updates:
5M active drivers × every 4 sec = 1.25M location writes/sec!
Matching requests:
10M trips/day = ~115 trip requests/secStep 3: Core Problem — Geospatial Matching
How do you find drivers within 2km of a rider efficiently?
NAIVE approach:
SELECT * FROM drivers WHERE lat BETWEEN ? AND ? AND lng BETWEEN ? AND ?
❌ Slow, no spatial indexing
SOLUTION: Geohash (or S2 cells like Uber uses)
Divide the world into a grid of cells
Each cell has a hash string: "9q8yy" = San Francisco area
Encode lat/lng → geohash prefix
Driver at (37.77, -122.41) → geohash "9q8yy"
Find nearby drivers:
1. Get geohash of rider location
2. Find 8 neighboring cells
3. Retrieve all drivers in those cells
✅ O(1) lookup from Redis!Step 4: Architecture
Example: Location Service updating Redis
javascript
import Redis from "ioredis";
const redis = new Redis();
async function updateDriverLocation(driverId, lat, lng) {
// Add or update the driver's location in the geospatial index
// The 'driver_locations' key holds the geospatial data
await redis.geoadd("driver_locations", lng, lat, driverId);
// Optionally set an expiry if a driver hasn't pinged recently
// This requires a separate ZSET or TTL key per driver in standard Redis
await redis.setex(`driver_status:${driverId}`, 10, "ONLINE");
console.log(`Updated location for driver ${driverId}`);
}Step 5: Location Update Flow
Every 4 seconds:
Driver App ──► Location Service ──► Kafka
│
┌───────────────┘
▼
Location Consumer
│
┌───────┴───────┐
▼ ▼
Redis (current Cassandra (historical
location + location trail for
geohash index) route/analytics)Step 6: Matching Flow
Example: Trip Matching Service querying Redis
javascript
import Redis from "ioredis";
const redis = new Redis();
async function findNearbyDrivers(riderLat, riderLng, radiusKm = 2) {
// Query Redis for drivers within the specified radius
// Returns array of arrays: [[driverId, distance], ...]
const nearbyDrivers = await redis.geosearch(
"driver_locations",
"FROMLONLAT",
riderLng,
riderLat,
"BYRADIUS",
radiusKm,
"km",
"WITHDIST", // Include distance in response
"ASC", // Sort nearest first
"COUNT",
10 // Limit to top 10 closest
);
const availableDrivers = [];
for (const [driverId, distance] of nearbyDrivers) {
// Check if the closer drivers are actually available/online
const status = await redis.get(`driver_status:${driverId}`);
if (status === "ONLINE") {
availableDrivers.push({ driverId, distance });
}
}
return availableDrivers;
}Step 7: Surge Pricing
Example: Calculating Surge Pricing
javascript
import Redis from "ioredis";
const redis = new Redis();
// This would typically run in a stream processor or a cron job every minute
async function calculateSurgeMultiplier(geohashCell) {
// In reality, these metrics might come from a time-series DB or stream aggregation
const demandCount = parseInt((await redis.get(`demand:${geohashCell}`)) || 0);
const supplyCount = parseInt((await redis.get(`supply:${geohashCell}`)) || 1); // Avoid div by 0
const ratio = demandCount / supplyCount;
let multiplier = 1.0;
if (ratio > 3.0)
multiplier = 2.0; // 2.0x surge
else if (ratio > 2.0)
multiplier = 1.5; // 1.5x surge
else if (ratio > 1.2) multiplier = 1.2; // 1.2x surge
// Store the new multiplier in Redis for quick access by pricing services
await redis.set(`surge_multiplier:${geohashCell}`, multiplier);
// Reset demand counters for the next window
await redis.set(`demand:${geohashCell}`, 0);
return multiplier;
}Step 8: Trip Lifecycle
Example: Trip State Machine
javascript
class TripStateMachine {
constructor(tripId) {
this.tripId = tripId;
this.state = "REQUESTED";
}
transition(event) {
switch (this.state) {
case "REQUESTED":
if (event === "DRIVER_ACCEPTED") this.state = "ACCEPTED";
if (event === "RIDER_CANCELLED") this.state = "CANCELLED";
break;
case "ACCEPTED":
if (event === "DRIVER_MOVING") this.state = "DRIVER_EN_ROUTE";
if (event === "DRIVER_CANCELLED") this.state = "REQUESTED"; // Rematch
break;
case "DRIVER_EN_ROUTE":
if (event === "DRIVER_ARRIVED") this.state = "ARRIVED";
break;
case "ARRIVED":
if (event === "TRIP_STARTED") this.state = "IN_TRIP";
break;
case "IN_TRIP":
if (event === "TRIP_ENDED") this.state = "COMPLETED";
break;
}
console.log(`Trip ${this.tripId} transitioned to ${this.state}`);
}
}
const trip = new TripStateMachine("trip_123");
trip.transition("DRIVER_ACCEPTED"); // ACCEPTED
trip.transition("DRIVER_MOVING"); // DRIVER_EN_ROUTE📊 Summary
| Component | Technology |
|---|---|
| Location Updates | Kafka + Redis GeoHash |
| Geospatial Index | Redis Geo (S2 Cells internally) |
| Driver ↔ App comms | WebSockets |
| Historical trips | Cassandra |
| Trip state machine | Redis + PostgreSQL |
| Route calculation | Google Maps API / OSRM |
| Surge pricing | Stream processing (Flink/Spark) |
Key insight: The geospatial indexing with geohash/S2 cells is the core performance trick. Without it, matching would require scanning millions of driver locations.
