A ride-sharing system is not merely a map with moving dots. It is a real-time marketplace that ingests noisy mobile telemetry, discovers plausible drivers, makes offers, and commits one physical-world assignment despite retries, timeouts, and partial failure. The useful design split is approximate candidate discovery versus authoritative reservation and trip state.
This walkthrough assumes the 6-step system design framework and applies it at senior depth. It is Part 7 of a system design series.
Table of Contents
- The Problem
- Step 1 - Clarify Requirements
- Step 2 - Estimate Scale
- Step 3 - API and Data Model
- Step 4 - High-Level Design
- Step 5 - Location and Spatial Indexing
- Step 6 - Matching, Failure, and Regions
- Reference Architecture
- Common Interview Mistakes
- Quick Reference
- Sources
- Frequently Asked Questions
- Related Articles
The Problem
Drivers publish their current location while online. A rider requests a vehicle for a pickup and destination. The system finds eligible supply, sends an offer, reserves one driver, and manages the ride through pickup, travel, completion, or cancellation.
The hard parts are coupled:
- mobile updates are duplicated, delayed, reordered, inaccurate, and sometimes malicious;
- a radius query must cover cell boundaries without scanning the fleet;
- the nearest driver may not have the best ETA or accept the request;
- a reservation crosses a database, notification channel, two apps, and real-world action;
- a dense station or stadium can overload one geographic partition;
- a timeout is ambiguous: the driver may have accepted while the reply was lost.
The design must therefore distinguish hints from authority. Location and availability indexes can be seconds stale; the assignment record cannot allow two active rides for one driver.
Step 1 - Clarify Requirements
Functional scope
- Drivers go online or offline and send location, heading, speed, accuracy, and timestamp.
- Riders request a ride with pickup, destination, service class, and an idempotency key.
- Dispatch discovers, filters, ranks, and offers the ride to eligible drivers.
- A driver accepts or declines; the rider sees the authoritative assignment.
- Both parties receive live trip state and appropriately reduced location updates.
- The trip supports cancellation, pickup, start, completion, and support investigation.
Pricing, payment settlement, route computation, and driver onboarding can be separate services, but their contracts matter. For example, ETA ranking needs a routing service, and a pricing quote needs an expiry and version.
Matching objective
Clarify whether the objective is shortest pickup ETA, high acceptance probability, fairness, accessibility, marketplace balance, or a batch-level optimum. “Nearest available driver” is a useful first version, not a production objective. The ranker must apply hard eligibility and safety rules before a business score.
Non-functional requirements
- Define SLOs separately for location freshness, candidate query latency, offer delivery, and time to confirmed match.
- Never create two active authoritative assignments for one driver.
- Make rider request, accept, decline, cancel, and complete operations idempotent.
- Bound work when supply is dense or sparse; radius expansion cannot fan out indefinitely.
- Continue active trips safely when new matching is paused.
- Encrypt precise location, minimize who can read it, and define retention by purpose.
Step 2 - Estimate Scale
Treat these as interview assumptions to validate, not industry facts:
- 5 million online drivers reporting every four seconds imply about 1.25 million updates per second before retries and reconnect bursts.
- 15 million rides per day imply about 174 ride creations per second on average; use a separately estimated peak and city distribution.
- 100 bytes per current location makes 500 MB of logical payload, but real memory includes keys, indexes, allocator overhead, expiry structures, replication, session metadata, and headroom.
- 1 KB per trip makes about 15 GB/day of logical trip records, before events, indexes, replicas, audit, and retention.
The traffic is highly skewed. Rush hour, airports, concerts, bad weather, and app reconnects produce correlated bursts. Drivers need not report at one fixed cadence: a stationary available driver can update less often, while an active pickup may update more frequently. Capacity tests need those distributions, out-of-order messages, shard moves, and failover.
The serving index and durable telemetry have different economics. Current position may be short-lived, while selected history might be retained for safety, fraud, billing, or support under a specific policy. “The index is ephemeral” does not mean “the business never persists location.”
Step 3 - API and Data Model
A persistent mobile transport can reduce connection overhead and carry server pushes, but REST, gRPC streams, WebSocket, or another protocol can all work. Frequency alone does not make HTTP request-response invalid. The logical contracts are more important:
LocationUpdate {
authenticatedDriverId, sessionEpoch, sequence,
latitude, longitude, accuracyMeters,
heading, speed, capturedAt
}
CreateRideRequest {
riderId, idempotencyKey, pickup, destination,
serviceClass, quoteId
}
RespondToOffer {
driverId, reservationId, fencingToken,
decision: ACCEPT | DECLINE
}Use server receive time as well as device capture time. A session epoch plus monotonic sequence lets ingestion ignore duplicate or reordered updates after reconnect. Validate coordinate range, maximum age, accuracy, impossible speed, and large jumps; suspicious telemetry can be excluded or down-ranked without exposing the fraud model.
Authoritative and derived state
| Record | Authority and durability |
|---|---|
| Driver session | Authenticated online/offline epoch; durable enough to recover ownership |
| Latest driver position | Short-lived serving state, versioned by session and sequence |
| Cell membership | Derived index; may contain stale candidates and is rebuilt |
| Ride request | Durable, idempotent workflow state |
| Driver reservation | Consistent authority with reservation ID, expiry, and fencing token |
| Trip | Durable lifecycle and participants |
| Outbox/inbox | Durable delivery intent and deduplication at controlled boundaries |
A cell membership saying “driver 42 is nearby” is a hint. Only the assignment store can say “driver 42 is currently reserved for request 99.”
Step 4 - High-Level Design
flowchart TD
Driver([Driver app]) -->|authenticated versioned locations| Ingest[Location ingestion]
Ingest --> Latest[(Latest-position store)]
Ingest --> Cells[(Derived spatial index)]
Rider([Rider app]) -->|idempotent ride request| Dispatch[Dispatch workflow]
Dispatch -->|candidate cells| Cells
Dispatch -->|freshness and eligibility| Latest
Dispatch --> ETA[Routing and ETA service]
Dispatch -->|conditional reservation| Assign[(Assignment store)]
Assign --> Outbox[(Offer outbox)]
Outbox --> Push[Connection and push layer]
Driver -->|accept with reservation token| Assign
Assign --> Trip[(Trip store and event outbox)]
Trip --> PushFigure 1. The spatial index discovers candidates, while a separate consistent assignment store owns reservations and accepted trips. Durable outboxes close database-to-delivery gaps.
This separation makes staleness safe. A stale cell may return a driver who moved or became busy; the latest-position filter and authoritative reservation simply reject that candidate. It is far more dangerous to combine volatile location membership, availability, and durable assignment in one undocumented field.
The dispatch workflow owns deadlines and retries. A push channel is only delivery: it can duplicate, delay, or lose an offer, so it cannot be the transaction boundary.
Step 5 - Location and Spatial Indexing
Ingestion and ordering
Partition updates by stable driver ID so one consumer normally sees a driver's events in order. Still enforce (sessionEpoch, sequence) at the latest-position store because retries, rebalances, and delayed old sessions happen. A newer session epoch fences an old phone connection; a delayed disconnect or location from the previous session must not mark the driver offline or move them backward.
Apply backpressure and coalescing. If the serving index is behind, intermediate points can often be dropped while the newest valid point is kept. Preserve a separate stream only if downstream safety or analytics consumers require it. Do not let a slow warehouse block live dispatch.
Index update correctness
Moving a driver changes both a latest-position record and cell membership. A naive “remove old, add new” across two shards can crash halfway. One practical derived-index design is:
- Atomically accept the newer latest-position version.
- Emit or process a versioned cell-change event.
- Add the new membership and lazily remove the old membership.
- At query time, deduplicate driver IDs and verify the current version, cell, freshness, and eligibility.
Temporary duplicates are safe because the query validates against the latest record. Periodic sweeping removes expired and orphaned memberships. If stronger index consistency is worth the cost, colocate latest state and membership or use a storage engine with a native geospatial index.
Covering a radius
A one-dimensional latitude or longitude index returns a large band. A spatial grid or tree narrows the scan to cells that can intersect the query shape.
flowchart LR
Pickup[Pickup point and radius] --> Cover[Conservative cell cover]
Cover --> Read[Read bounded cell memberships]
Read --> Dedupe[Deduplicate driver IDs]
Dedupe --> Filter[Check latest version, freshness, eligibility]
Filter --> Distance[Exact spherical distance]
Distance --> Rank[ETA and dispatch ranking]Figure 2. Cells are a conservative candidate filter. Exact distance, current state, and business eligibility are checked after the cell read.
The classic cell-edge bug is querying only the pickup cell. But “always read the eight neighbors” is also wrong: a 2 km circle can intersect more than a 3×3 neighborhood when cells are small, and cell dimensions vary with the grid and position. Compute a conservative cover for the chosen radius and resolution, then exact-filter candidates. Expand the radius or grid distance only within a deadline, cell limit, and candidate budget.
Geohash, quadtree, H3, and S2
Geohash creates hierarchical rectangular keys by interleaving longitude and latitude bits. Prefixes are operationally convenient, but geometrically close points across a boundary can have unrelated prefixes, and cell shape changes with latitude.
Quadtree recursively subdivides dense cells. It can adapt the index to demand, but dynamic points still require versioned ownership, split/merge thresholds, hysteresis, and online migration. “Quadtree for static data only” is too simple; the operational cost is the real trade-off.
H3 is a hierarchical hexagonal grid on the sphere. Its gridDisk API returns cells within k grid steps, which is useful for candidate expansion, but grid distance is not exact road or great-circle distance. H3 has 12 pentagons at every resolution, traversal has distortion cases, and even hexagon areas vary by location.
S2 maps the sphere onto six cube faces and recursively subdivides cells; cell IDs follow a Hilbert curve and support hierarchy and region covering. It is another valid library choice, not proof that a custom index will scale automatically.
| Choice | Useful property | Operational caveat |
|---|---|---|
| Geohash | Simple hierarchical key/prefix | Boundary and latitude geometry |
| Quadtree | Density-adaptive subdivision | Split, merge, ownership, and migration |
| H3 | Hexagonal hierarchy and grid traversal | Pentagons, distortion, non-metric grid steps |
| S2 | Spherical hierarchy and region covering | Library complexity and covering trade-offs |
| Native geo store | Built-in radius queries | Benchmark update/query skew and shard semantics |
Redis GEOSEARCH is one example of a native radius or box query. Its documented cost depends on candidates in the bounding box and items in the index, so dense areas and result sorting still need capacity limits.
Hot zones and partitioning
Geography is local but not balanced. A stadium cell and rural cells can differ by orders of magnitude. Use a directory that maps logical cells to versioned shard owners, split dense cells, and balance on update rate, query rate, candidate count, CPU, and memory rather than land area.
When ownership moves, use routing epochs, dual-read or forward during a bounded transition, and fence stale writers. A nearby query may touch several shards, so enforce a fan-out deadline and tolerate partial candidate results with observability rather than waiting forever.
Step 6 - Matching, Failure, and Regions
Candidate generation and ranking
The pipeline is normally:
- Cover the initial pickup radius with cells and fetch a capped candidate set.
- Deduplicate and reject stale, inaccurate, offline, reserved, wrong-class, or policy-ineligible drivers.
- Obtain ETA for a bounded shortlist, not every point in a dense cell.
- Rank by ETA, acceptance probability, driver idle time, fairness, accessibility, and marketplace policy.
- Reserve and offer to one driver, or use a clearly defined bounded parallel/batch policy.
- Expand the search if no candidate accepts before the workflow deadline.
Distance is a cheap prefilter. Road network, traffic, direction, pickup restrictions, and bridges make ETA the more useful final proximity signal.
Reservation and acceptance
The claim must live in a consistent store, not the derived spatial index. A simplified reservation row can use driverId as a uniqueness key and contain reservationId, rideRequestId, status, expiresAt, and a monotonically increasing fencingToken.
sequenceDiagram
participant M as Dispatch workflow
participant A as Assignment store
participant O as Offer delivery
participant D as Driver app
M->>A: reserve driver if AVAILABLE or expired
A-->>M: reservation R, token 81 committed with outbox
O-->>D: offer R, token 81
D->>A: accept R, token 81
A->>A: compare reservation and token, create assignment/trip event
A-->>D: accepted
Note over M,A: timeout releases only R with token 81Figure 3. Reservation identity and fencing make retries safe: an old timeout or delayed accept cannot mutate a newer offer for the same driver.
Reserve the driver and write the offer outbox in one transaction. A publisher can then retry delivery without losing committed intent. Acceptance is idempotent and succeeds only if the same reservation and token are still active; it atomically transitions to ASSIGNED and creates the durable trip or its outbox event. A timeout releases only the exact reservation and token it created.
TTL is not sufficient by itself. Expiry must use trusted store time and a state transition or query predicate; delayed timers are normal. If a driver's acceptance arrives after expiry, the server returns the authoritative result rather than letting the app assume success. If a network response is lost, the app queries by reservation or ride-request ID.
This is not end-to-end exactly-once delivery. It is at-least-once messaging around idempotent, fenced state transitions inside databases the service controls.
Trip lifecycle
Model transitions explicitly, for example:
REQUESTED -> SEARCHING -> OFFERED -> ASSIGNED
ASSIGNED -> DRIVER_ARRIVING -> AT_PICKUP -> IN_PROGRESS -> COMPLETED
any permitted state -> CANCELLEDValidate the actor, current state, transition, and version. Cancellation after assignment may trigger compensation, fees, re-dispatch, or support workflow; it is not a rollback that erases history. Append audit events or use an immutable event/outbox record alongside the current state.
Failure recovery
- Location node loss: fail over to a replica and rebuild from active-session state, a recent snapshot, or a retained update stream. Waiting for every device to report again gives an unbounded blind spot and a reconnect herd, not a guaranteed four-second recovery.
- Dispatch crash: the durable ride request, reservation, deadlines, and outbox let another worker resume idempotently.
- Offer-delivery outage: reservations expire or are deliberately released; cap outstanding offers so drivers are not locked by a broken channel.
- Routing/ETA outage: use a documented distance-based fallback or pause new matching if its quality is unsafe.
- Assignment-store slowdown: shed new requests and preserve active trips; do not bypass the authority with an unchecked local claim.
- Clock skew: use store time for leases and deadlines, and monitor mobile capture-time skew separately.
Multiple regions
Markets provide a useful routing boundary, not complete independence. A city needs multi-zone redundancy; adjacent pickup cells can cross shard or market boundaries; travelers and long trips cross jurisdictions; identity, pricing, support, and fraud services may be global.
Give each market or assignment shard one fenced write authority at a time. Replicate durable ride and assignment state according to explicit RPO and RTO. During regional failover, stop or queue new reservations until the new owner has a higher epoch and the old owner is fenced. Active-trip messaging can reconnect to the new region and resynchronize from durable trip state.
Location state may be rebuilt or asynchronously replicated because it is a hint, but its loss changes match quality and capacity. Data residency and precise-location access may constrain where snapshots, telemetry, and support data can move.
Security, privacy, and observability
Authenticate driver devices and rotate session credentials. Rate-limit updates and ride requests; validate pickup access and service area; detect GPS spoofing and impossible movement; authorize every trip-state subscription so a user cannot track arbitrary drivers or riders.
Use different precision and retention for dispatch, rider display, analytics, and support. Encrypt data in transit and at rest, audit privileged reads, and avoid exposing a driver's exact pre-match location to riders.
Measure:
- location freshness by market, session reconnects, stale/out-of-order drops, and index lag;
- candidate counts before and after filtering, cell/shard fan-out, partial queries, and hot-zone skew;
- time to first offer and confirmed match, acceptance/decline/timeout rates, and radius expansions;
- reservation contention, expired offers, stale-token rejects, duplicate requests, and workflow age;
- assignment-store and outbox latency, regional epoch changes, rebuild progress, and unmatched demand;
- fairness, cancellation, fraud, and safety guard outcomes without leaking raw location into labels.
An SLO such as “99% matched within five seconds” is meaningful only with market, service class, supply conditions, and exclusion rules defined.
Reference Architecture
The reusable pattern is:
A rebuildable spatial projection discovers candidates from versioned telemetry; a separate consistent workflow reserves the scarce resource with idempotency, fencing, and durable delivery intent.
flowchart LR
Telemetry[Versioned moving-point telemetry] --> Projection[(Rebuildable spatial projection)]
Projection --> Candidates[Bounded candidate discovery]
Candidates --> Authority[(Consistent reservation authority)]
Authority --> Workflow[Durable assignment workflow]
Workflow --> ProjectionFigure 4. Approximate, rebuildable discovery stays separate from the authoritative reservation workflow.
The same shape appears in delivery dispatch, field-service scheduling, warehouse picking, nearby inventory, and booking a limited physical resource.
Common Interview Mistakes
- Querying only one cell, or assuming a fixed 3×3 neighborhood covers every radius.
- Claiming nearby geohash prefixes guarantee geometric proximity across boundaries.
- Treating geography as an even partition and ignoring venues, airports, and rush-hour skew.
- Keeping driver availability and the only assignment claim inside a volatile spatial index.
- Using CAS without reservation identity, fencing, durable offer intent, or safe timeout release.
- Assuming a TTL fires exactly on time or that a lost accept response means the accept failed.
- Saying all locations need no durability without separating the serving index from safety or fraud telemetry.
- Letting old sessions, reordered updates, or spoofed coordinates overwrite a newer position.
- Ranking solely by straight-line distance and calling it dispatch quality.
- Calling each city independent without a multi-zone, failover, boundary, or data-residency story.
Quick Reference
| Topic | Senior-level answer |
|---|---|
| Location updates | Authenticated session epoch + sequence; coalesce and reject stale data |
| Spatial query | Conservative cell cover, bounded fan-out, exact distance and freshness filter |
| Grid choice | Geohash, quadtree, H3, S2, or native store based on workload and operations |
| Availability | Derived serving index is replicated and rebuildable, not magically self-healing |
| Ranking | Eligibility first; ETA and marketplace objectives after a bounded shortlist |
| Reservation | Consistent conditional write with reservation ID, expiry, and fencing token |
| Delivery | Reservation + outbox transaction; at-least-once push with idempotent response |
| Timeout | Conditional release of the same token; trusted store time |
| Hot zones | Versioned directory, adaptive split, online migration, and fan-out deadline |
| Multi-region | Fenced market authority with explicit active-trip RPO and RTO |
Sources
- H3 4.x: system overview - hierarchical spherical grid, resolutions, and pentagons.
- H3 4.x: grid traversal -
gridDisk, grid distance, output ordering, and distortion limitations. - H3 4.x: cell statistics - position-dependent cell area and average edge lengths by resolution.
- S2 Geometry: S2 cell hierarchy - spherical cube-face projection, Hilbert ordering, and hierarchical cell IDs.
- Redis
GEOSEARCH- radius/box query semantics, result limits, sorting, and documented complexity.
Frequently Asked Questions
How do you find nearby drivers efficiently?
Use a spatial index to retrieve a bounded candidate set from every cell that can intersect the search radius, then deduplicate and filter by exact spherical distance, freshness, vehicle type, and eligibility. Rank survivors by ETA and marketplace policy rather than distance alone. Expand the radius or cell ring under a strict candidate and latency budget when supply is sparse.
What is the difference between geohash, quadtree, H3, and S2?
Geohash gives simple hierarchical rectangular keys but has boundary and latitude-dependent geometry. A quadtree adapts subdivision to density but needs ownership and migration rules as points move. H3 and S2 provide hierarchical spherical cells with useful traversal or covering APIs, yet still have distortion and special cases. Choose from query shapes, update cost, libraries, and operational model.
How do you handle the firehose of driver location updates?
Authenticate each driver session, attach a monotonically increasing sequence or session epoch, reject stale and implausible updates, and keep only the latest position in a partitioned in-memory index. Make the serving index replicated and rebuildable from active-session state, snapshots, or an update stream. Persist separate telemetry only when safety, fraud, support, or legal requirements justify its cost and retention risk.
How does ride matching avoid assigning one driver to two riders?
Treat the spatial index as candidate discovery only. A consistent assignment store conditionally changes a driver from available to reserved and records the reservation ID, ride request, expiry, and fencing token in the same transaction as a delivery outbox. Driver acceptance must compare the same reservation and token before atomically creating the assignment or trip, so a stale timeout cannot release or accept a newer reservation.
What is the geospatial cell-edge problem?
A nearby point may be in another cell even when its key shares no useful prefix with the query point. Query all cells whose geometry can intersect the radius, not blindly the center cell plus eight neighbors; the required cover depends on radius, resolution, latitude, and grid system. The cell cover is only a candidate filter, so finish with exact distance and freshness checks.
How is a ride-sharing system partitioned across regions?
Geographic routing reduces most matching to one market, but it does not make regions independent. Boundary queries can fan out, dense venues create hot partitions, cities need multi-zone redundancy, and failover must fence the old assignment authority. Define home-market routing epochs, replicated durable state, location-index recovery, data residency, and explicit RPO and RTO for active trips and new matches.
Related Articles
- System Design Interview Problems: A Senior's Roadmap - the full series index and pattern library.
- System Design Interview Guide: The 6-Step Framework - the method this walkthrough applies.
- Design a Distributed Cache - Part 4; rebuildable projections, sharding, and hot keys.
- Design a Chat System - Part 6; persistent sessions, fencing, delivery, and resynchronization.
- Design a Web Crawler - Part 8; partitioned work, retries, and backpressure.
- Design a Payment System - Part 11; idempotent workflows, outbox, and reconciliation.
This is Part 7 of the core track in a 16-part system design series. Next: Design a Web Crawler.
