Skip to content

Ride-Sharing System Design: Location Updates, Matching and Dispatch

Design a ride-hailing backend step by step: ingesting driver locations, a geospatial index, matching and dispatch without double booking, and the trip state machine.

· Founder & developer

Published

Architecture diagram in a city frame. The driver app sends locations through a gateway to Kafka, which feeds a Redis geo index and an object store. The rider app requests a ride; matching queries the geo index and an ETA service and offers it; the trip service stores states in Postgres.

A rider taps "Request" and expects a car within seconds. Behind that tap, the system has to know where hundreds of thousands of moving drivers are right now, pick a good one, make sure no other rider gets the same driver, and then track a trip that ends with a payment. It is a favorite system design interview question because it mixes a high-volume real-time stream with a small, strict transactional core.

This post designs the backend step by step: requirements and capacity numbers, location ingestion, geospatial indexing with geohash, S2 and H3, matching and dispatch, the trip state machine, and the failures you have to plan for, with an architecture diagram you can copy.

Requirements

Functional:

  • Drivers go online and stream their location.
  • Riders request a ride and see nearby cars and an estimated pickup time.
  • The system matches a rider with a driver, who can accept or decline.
  • Both apps track the trip live; the trip ends with a fare and a payment.

Non-functional:

  • A match within a few seconds.
  • Driver locations no more than a few seconds stale.
  • Never assign one driver to two riders.
  • One city's problems must not take down other cities.

Capacity: the numbers that shape the design

Assume, for a large deployment, one million drivers online at peak, each sending a location every 4 seconds, and 20,000 ride requests per minute:

Quantity Calculation Result
Location updates 1,000,000 / 4 s 250,000 per second
Ingest bandwidth 250,000 × ~100 bytes ~25 MB/s
Raw history at peak rate 25 MB/s × 86,400 s ~2 TB per day
Ride requests 20,000 / 60 s ~333 per second

Two very different workloads appear. Location updates are enormous in volume, but each one is small, disposable and superseded 4 seconds later. Ride requests are three orders of magnitude rarer, but each one must be handled exactly once and tied to money. The architecture separates them.

Location ingestion: keep only what is current

Drivers' apps send locations over a persistent connection, such as a WebSocket, or as small periodic HTTPS requests. A location gateway authenticates the driver, drops impossible points (a jump of 50 km in 4 seconds), and writes each update to a stream such as Kafka, partitioned by city.

Three consumers read the stream:

  1. The live geospatial index keeps only the latest position of each available driver, with a short expiry so a driver whose phone went silent disappears after, say, 30 seconds.
  2. Trip tracking forwards the assigned driver's position to the rider's app.
  3. Cold storage keeps the full history in object storage for analytics, ETA models and dispute resolution.

The key decision is that the hot index holds one row per driver, not one row per update. A million drivers at roughly a hundred bytes each is around 100 MB, small enough to keep in memory per city.

Geospatial indexing

The matching question is "which available drivers are near this point?" A plain database index on latitude and longitude answers it poorly, so systems map coordinates to cells.

Index Cell shape Strengths Watch out for
Geohash Rectangles, encoded as a string prefix Simple; prefixes nest; Redis GEO uses it Cells vary in shape; neighbors can have different prefixes
S2 Quadrilaterals on a projected cube Hierarchical, used by Google More complex to reason about
H3 Hexagons Every neighbor is at the same distance; 16 resolutions Hexagons do not nest perfectly

H3, which Uber open-sourced in 2018, makes the radius search uniform: take the rider's cell and the ring of cells around it, then collect the drivers inside. Resolution matters. From the H3 resolution table:

H3 resolution Average cell area Average edge length
7 5.16 km² 1.41 km
8 0.74 km² 0.53 km
9 0.11 km² 0.20 km

Resolution 8 or 9 suits dense cities; rural areas need wider rings or coarser cells.

If you want to start simple, Redis has geospatial commands built in:

GEOADD drivers:sf -122.4194 37.7749 driver:8812
GEOSEARCH drivers:sf FROMLONLAT -122.4100 37.7800 BYRADIUS 2 km ASC COUNT 20 WITHDIST

GEOSEARCH, available since Redis 6.2, returns the 20 nearest drivers within 2 km, sorted by distance. Shard by city first, the natural boundary, and split very busy places such as airports or stadiums into finer cells.

Matching and dispatch

Straight-line distance is a poor proxy for pickup time: a driver 500 m away across a river may be 15 minutes out. Matching therefore works in two stages:

  1. Candidate generation. Query the geo index for available drivers near the pickup point, filtered by vehicle type and other constraints.
  2. Ranking. Ask an ETA service for road-network pickup times for the top candidates, and rank by ETA, plus factors such as the driver's heading.

Matching each request instantly to the nearest driver is greedy. Collecting requests for a second or two and solving them together as an assignment problem minimizes total waiting time across riders. The trade-off is a short delay in exchange for better matches during busy periods.

Then dispatch sends an offer to one driver at a time, with a timeout of around 15 seconds. On decline or timeout, the next candidate gets the offer.

Never double-book a driver

Two matching workers can pick the same driver for two riders within the same second. Prevent it at the driver's state, not in the matching logic:

  • Store each driver's state (available, offered, on_trip) with a version number.
  • An offer is a conditional update: set offered only if the state is still available at the version you read.
  • Exactly one worker wins; the other moves to its next candidate.
  • An expired offer moves the driver back to available, again conditionally.

The same compare-and-set idea protects the rider side: one active request per rider.

The trip state machine

A trip moves through requested, matching, driver_assigned, driver_arrived, in_progress, completed and paid, with cancellation possible from several states. The trip service stores each trip in a relational database such as Postgres, applies transitions only when the current state allows them, and publishes every change as an event for billing, notifications, receipts and analytics.

Make every transition idempotent, keyed by trip ID and state version. Mobile networks retry, and "driver arrived" delivered twice must not do anything twice. The state diagram example shows how to draw this lifecycle on its own.

Live updates, ETAs and pricing

While a trip is active, the rider's app receives the driver's position through a push connection, throttled to every few seconds and smoothed on the client. The same location stream feeds the ETA service, which needs live traffic, and surge pricing, which compares requests with available drivers per cell over a sliding window. Payment happens after the trip through a payment provider; the payment gateway integration diagram covers that part.

Failures to plan for

  • Drivers lose connectivity. The app sends only the latest point when it reconnects, and the index expires drivers that go quiet.
  • A matching worker crashes mid-offer. Offers carry expiry times; the driver returns to available when the offer lapses.
  • A city's cluster fails. Because everything is partitioned by city, the failure stays local. Trips in progress keep their state in the durable trip store.
  • Spoofed GPS. Plausibility checks on speed and route at the gateway catch the crude cases.

The ride-sharing diagram

Architecture diagram in a city frame. The driver app sends locations through a gateway to Kafka, which feeds a Redis geo index and an object store. The rider app requests a ride; matching queries the geo index and an ETA service and offers it; the trip service stores states in Postgres.

12 components · 10 connections

Open in Archboard
Diagram contents
  • City partition: everything inside is per city
  • Driver app
  • Rider app
  • Location gateway
  • locations topic
  • Live geo index (latest, 30 s TTL)
  • Location history
  • Matching + dispatch
  • ETA service
  • Trip service
  • Trips (Postgres)
  • GPS every 4 s
  • 250k/s
  • latest position
  • every point
  • POST /rides
  • nearby available drivers
  • rank by pickup ETA
  • driver assigned (CAS)
  • state transitions
  • ride offer, 15 s to accept
A ride-hailing backend for one city. Driver locations flow through a gateway into a stream and a live geospatial index; a ride request goes to matching, which ranks nearby drivers by ETA and sends one offer at a time; the trip service records every state change.

How to read it:

  • The frame is one city, the partition boundary for everything inside it.
  • The location path runs along the top: driver app, location gateway, Kafka, and from there the Redis geo index and the object store for history.
  • The request path runs below: the rider app calls matching, which reads candidates from the geo index, asks the ETA service for pickup times and sends the offer to the driver.
  • The trip service owns the state machine and stores it in Postgres.
  • The Markdown block shows the capacity numbers.

Drawing system design diagrams for real-time systems

  • Separate the hot path from the transactional path. Here, the location stream and the trip lifecycle have different volumes, guarantees and failure modes; draw them as separate lanes.
  • Write the partition key on the diagram. "Partitioned by city" explains scaling and failure isolation in three words.
  • Show what is current and what is history. The latest-position index and the history store answer different questions.
  • Label edges with rates: 250,000 updates per second on one arrow tells reviewers where the load is.

For a general approach to these diagrams, see the system design diagram example. Push connections are covered in the chat app architecture diagram, and the stream itself in the Kafka architecture diagram.

Build it in Archboard

  1. Sign in with Google or GitHub and create a diagram.
  2. Add a Core Frame named for the city.
  3. Add two Device nodes from the Network pack for the driver app and the rider app.
  4. Add an API Gateway from the Cloud pack for the location gateway, a Kafka node from the Data pack for the stream, a Redis node for the geo index and an Object Store for history.
  5. Add three Compute nodes from the Cloud pack for the matching, ETA and trip services, and a Postgres node from the Data pack for trips.
  6. Connect the location path along the top and the request path below it, and label edges with what they carry and how often.
  7. Add a Markdown block from the Core pack with the capacity numbers.

The example uses 12 diagram nodes, within the Free plan.

FAQ

How often should drivers send their location? Every few seconds is a common range. More frequent updates make positions fresher but multiply ingest volume: halving the interval from 4 to 2 seconds doubles the traffic.

Should I use geohash, S2 or H3? Any of them works. Geohash is the simplest and built into Redis; H3 gives uniform neighbor distances, which makes radius searches and per-cell pricing cleaner. Pick one and use it consistently.

How do you avoid assigning one driver to two riders? Make the offer a conditional update on the driver's state, so it succeeds only if the driver is still available at the version the matcher read. Only one matcher can win.

Why not match each rider to the nearest driver immediately? Greedy matching can leave later riders with long waits. Batching requests for a second or two and solving them together lowers the total waiting time, at the cost of a short delay.

What database stores trips? A relational database fits well: trips are transactional, need strict state transitions and feed billing. The high-volume location data lives elsewhere, in memory and in object storage.

Start free — one seat, no credit card.

Related posts

← Back to blog

Open a canvas in a minute

Sign in with Google or GitHub. Free for a single seat. Upgrade when the team joins.