System Design course menu
System Design: Ride-Sharing App Like Uber or Ola
Design a ride-hailing system: tracking driver locations at scale, geohash and grid indexes for nearby search, matching riders with drivers, and trip state machines.
On this page
Ride-sharing is about location data that changes every few seconds, and matching people in real time.
1. Requirements#
Functional: riders request a trip, nearby drivers receive it, a driver accepts, both see live locations, and the trip ends with a payment.
Non-functional: matching within seconds, no rider or driver matched twice, and high availability.
2. Estimates#
active_drivers = 1_000_000
update_seconds = 4
rides_per_day = 20_000_000
print(f"Location updates: {active_drivers / update_seconds:,.0f}/s")
print(f"Ride requests: {rides_per_day / 86_400:,.0f}/s on average")Location updates: 250,000/s
Ride requests: 231/s on averageLocation updates are the biggest load. They are frequent, and each one is soon out of date, so we do not need to keep them all in a slow database.
3. Keeping driver locations#
Store only the latest position in memory, such as in Redis, and send raw updates to a stream for later analysis:
Driver app --> Location service --> Redis (latest position)
--> Kafka --> Trip history, analyticsLosing an update does not matter, since another arrives in seconds.
4. Finding nearby drivers#
Checking the distance to all drivers is far too slow. Instead, divide the map into cells, and index drivers by cell. Nearby locations share a cell or sit in neighbouring ones. Geohash does this, and so do Google's S2 and Uber's H3 systems.
This is a simple grid. A real system also handles the curve of the Earth:
import math
from collections import defaultdict
CELL = 0.01
def cell(lat, lon):
return (int(lat / CELL), int(lon / CELL))
def km(a, b):
dx = (a[1] - b[1]) * 111 * math.cos(math.radians(a[0]))
dy = (a[0] - b[0]) * 111
return math.hypot(dx, dy)
drivers = {"d1": (17.385, 78.486), "d2": (17.391, 78.479), "d3": (17.440, 78.350), "d4": (17.380, 78.495)}
grid = defaultdict(list)
for name, pos in drivers.items():
grid[cell(*pos)].append(name)
def nearby(pos, rings=1):
cx, cy = cell(*pos)
found = []
for x in range(cx - rings, cx + rings + 1):
for y in range(cy - rings, cy + rings + 1):
found += grid.get((x, y), [])
return sorted(found, key=lambda d: km(pos, drivers[d]))
rider = (17.384, 78.487)
for d in nearby(rider, rings=1):
print(f"{d}: {km(rider, drivers[d]):.2f} km")
print("Cells searched: 9, drivers checked:", len(nearby(rider, rings=1)), "of", len(drivers))d1: 0.15 km
d4: 0.96 km
d2: 1.15 km
Cells searched: 9, drivers checked: 3 of 4We checked only the rider's cell and its neighbours, and d3, far away, was never considered. If too few drivers are found, the search widens.
5. High-level design#
Rider app --> API gateway --> Trip service --> Trips DB (relational)
|
Driver app <-- WebSocket <---- Matching service <-- Location store (Redis)
|
Payment, notifications (via a queue)Shard location data by city or region. Cities are mostly independent, so each region can run separately, which also limits the damage from a failure.
6. Matching#
- Find nearby available drivers.
- Rank them by estimated arrival time, not straight-line distance.
- Offer the trip to the best driver, with a short timeout.
- If they decline or time out, try the next.
The system must not give one driver two trips. Use an atomic check:
state = {"d1": "available"}
def try_assign(driver, trip):
if state[driver] == "available":
state[driver] = f"on {trip}"
return True
return False
print("Trip A:", try_assign("d1", "A"))
print("Trip B:", try_assign("d1", "B"))
print(state)Trip A: True
Trip B: False
{'d1': 'on A'}In a real system this check and update must happen as one step, using a Redis script, a database transaction, or a lock with a short expiry. Otherwise, two servers could pass the check at the same moment.
7. Trip state#
A trip moves through fixed states, which prevents invalid changes:
REQUESTED --> MATCHED --> DRIVER_ARRIVED --> IN_PROGRESS --> COMPLETED
\ \
+------------+--> CANCELLEDTrips involve money, so keep them in a strongly consistent relational database. Payments should be idempotent, so that a retry never charges twice.
8. Other parts#
- ETA and routing: a road-graph service, updated with live traffic.
- Surge pricing: compares demand and supply per cell.
- Live tracking: the driver's position is pushed to the rider over a WebSocket.
Trade-offs to mention#
- Update more often for accuracy, or less often to save load and battery.
- In-memory locations are fast, and losing some is acceptable.
- Trips need strong consistency, and locations do not.
Key takeaway#
Finished this lesson?
Related lessons
Recommended System Design resources
- System Design Interview by Alex Xu
Step-by-step walkthroughs of common design questions, for interview preparation.
Some links are affiliate links. If you buy through them, Laviyx Learn may earn a commission at no extra cost to you. As an Amazon Associate, we earn from qualifying purchases.