Skip to content
Laviyx Learn

Try

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.

Advanced7 min readUpdated
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#

Python
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")
Output
Location updates: 250,000/s
Ride requests:    231/s on average

Location 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:

Text
Driver app --> Location service --> Redis (latest position)
                                --> Kafka --> Trip history, analytics

Losing 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:

Python
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))
Output
d1: 0.15 km
d4: 0.96 km
d2: 1.15 km
Cells searched: 9, drivers checked: 3 of 4

We 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.

IndexIdeaGood for
GeohashMap location to a text prefixEasy with Redis (GEOSEARCH)
QuadtreeSplit busy areas into smaller cellsUneven density
Hexagon grid (H3)Even distance to neighboursPricing, demand maps

5. High-level design#

Text
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#

  1. Find nearby available drivers.
  2. Rank them by estimated arrival time, not straight-line distance.
  3. Offer the trip to the best driver, with a short timeout.
  4. If they decline or time out, try the next.

The system must not give one driver two trips. Use an atomic check:

Python
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)
Output
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:

Text
REQUESTED --> MATCHED --> DRIVER_ARRIVED --> IN_PROGRESS --> COMPLETED
     \            \
      +------------+--> CANCELLED

Trips 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#

Next: Design a Ticket Booking System.

Finished this lesson?

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.