Markdown Editor

Ride-Hailing Dispatch System Design Template (Reference Design)

Ride matching: location ingestion, H3/geohash index, batched matching, ETA, trip state machine, single assignment, surge note

Cara menggunakan: Use when building dispatch or any real-time supply-demand matching. Size per largest city, not only globally. The main pitfall is double-assigning a driver; enforce a single writer per shard and compare-and-set on driver state.

Pratinjau

System Design: [Ride-hailing dispatch name]

Reference design for ride-hailing dispatch: ingesting driver locations, indexing them geospatially (geohash or H3), matching riders to drivers, estimating arrival times, managing the trip lifecycle and guaranteeing that a driver is never assigned to two trips. It includes a note on dynamic (surge) pricing. It follows the structure of the System Design Document template and is pre-filled with a worked design to adapt. Values marked Example are assumptions; local transport regulation may constrain pricing and data use, so treat those parts as a format, not legal advice.

Document control

FieldValue
Document ID[SDD-DISPATCH-001]
System / feature[Rider-driver matching and trip lifecycle]
Version[0.1]
Status[Draft / In review / Approved / Implemented / Superseded]
Author(s)[@author]
Reviewers[@reviewer-1], [@reviewer-2]
Last updatedYYYY-MM-DD
Related[Marketplace PRD], [Pricing policy], [Privacy impact assessment]

Approvals

RoleNameDecisionDate
Tech lead[@name][Approved / Changes requested]YYYY-MM-DD
Staff / principal engineer[@name][Decision]YYYY-MM-DD
SRE / operations[@name][Decision]YYYY-MM-DD
Marketplace / data science lead[@name][Decision]YYYY-MM-DD
Privacy and legal[@name][Decision]YYYY-MM-DD

Revision history

VersionDateAuthorChange summary
0.1YYYY-MM-DD[@name]First draft for design review
0.2YYYY-MM-DD[@name]Example: Batched matching every 2 s instead of greedy nearest driver
0.3YYYY-MM-DD[@name]Example: City-sharded dispatch with single writer per city cell group
1.0YYYY-MM-DD[@name]Example: Approved

Contents

1. Overview

1.1 Problem statement

[Example: Matching queries a relational table of driver positions with a bounding box. At evening peak in our largest city the query takes 900 ms, two dispatchers sometimes offer the same driver to two riders, and the nearest driver by straight line is often across a river, 14 minutes away.]

1.2 Proposed solution

[Example: Driver apps send locations every 4 seconds to an ingestion gateway. A location service keeps the latest position and state of every online driver in memory, indexed by H3 cell, sharded by city. When a rider requests a trip, the dispatch service for that city finds candidate drivers in nearby cells, ranks them by road-network ETA from an ETA service, and assigns in small batches to minimise total pickup time. Assignment is a compare-and-set on the driver's state, so a driver can hold only one offer. The trip service drives a trip state machine and notifies both apps.]

1.3 Decisions requested from reviewers

IDQuestionRecommended optionSection
Q-01Example: Geohash, S2 or H3?H3 at resolution 9 for supply, 7 for pricing7.2, 10
Q-02Example: Greedy or batched matching?Batched every 2 s, greedy fallback under load7.3
Q-03Example: How is double assignment prevented?Single writer per city shard + CAS on driver state with offer TTL7.5
Q-04Example: Location history retention?30 days raw, then aggregated and de-identified9.2
[Q-nn][Question][Option][Section]

2. Requirements

2.1 Functional requirements

IDRequirementPriority
FR-01Example: Drivers go online and offline and share location while onlineMust
FR-02Example: Riders see nearby drivers and an ETA before requestingMust
FR-03Example: Request a ride; get a driver offer accepted within 30 s or a clear "no drivers"Must
FR-04Example: Drivers accept or decline offers within 15 sMust
FR-05Example: Live trip tracking for rider and driverMust
FR-06Example: Cancellation by either side with defined feesMust
FR-07Example: Dynamic pricing per area based on supply and demandShould
[FR-nn][Requirement][Must / Should / Could]

2.2 Non-functional requirements

IDQualityTargetMeasured as
NFR-01Matching latencyExample: Request to first offer p95 at or below 3 sDispatch metrics
NFR-02CorrectnessExample: Zero drivers holding two active offers or tripsInvariant checker
NFR-03Location freshnessExample: Driver position used for matching at most 10 s oldAge at match time
NFR-04AvailabilityExample: 99.99% for ride requests per citySynthetic requests
NFR-05ETA accuracyExample: Median absolute error at or below 1 min for pickups under 10 minPredicted vs. actual
[NFR-nn][Quality][Target][Measurement]

2.3 Out of scope

  • [Example: shared rides (pooling) with multiple pickups]
  • [Example: scheduled rides booked days ahead]
  • [Example: payments (handled by the payments platform)]
  • [Item deferred, with the release that will take it]

3. Capacity estimation

3.1 Assumptions

InputSymbolValueSource
Online drivers at global peakDDExample: 1 millionSupply data
Location update intervalΔ\DeltaExample: 4 sApp setting
Trips per dayTTExample: 20 millionDemand data
Peak to average factorkkExample: 3Evening peak
Candidates scored per requestccExample: 107.3
Location record (ID, lat, lng, heading, speed, time, state)ssExample: 50 bytesSchema
Largest city share of global peakffExample: 8%Market data

3.2 Location ingestion

U=DΔ=1064=250 000 updates/sU = \frac{D}{\Delta} = \frac{10^{6}}{4} = 250\,000 \ \text{updates/s}
Bin=U×s=250 000×50 B=12.5 MB/sB_{in} = U \times s = 250\,000 \times 50 \ \text{B} = 12.5 \ \text{MB/s}

Latest-position state in memory: 106×10010^{6} \times 100 B (with index overhead) =100= 100 MB globally, about 8 MB for the largest city. Memory is not the constraint; update rate and fan-out to rider apps are.

3.3 Requests and ETA calls

QPSreq=T86 400×k=2×10786 400×3≈700 requests/s at peakQPS_{req} = \frac{T}{86\,400} \times k = \frac{2 \times 10^{7}}{86\,400} \times 3 \approx 700 \ \text{requests/s at peak}
QPSeta=QPSreq×c≈7 000 ETA computations/sQPS_{eta} = QPS_{req} \times c \approx 7\,000 \ \text{ETA computations/s}

plus pre-request ETA previews (Example: 5 per request) at about 3 500/s. In the largest city, f×700≈56f \times 700 \approx 56 requests/s.

3.4 Location history

Sday=12.5 MB/s×86 400 s≈1.08 TB/day(upper bound at constant peak)S_{day} = 12.5 \ \text{MB/s} \times 86\,400 \ \text{s} \approx 1.08 \ \text{TB/day} \quad (\text{upper bound at constant peak})

Thirty days of raw history is at most about 32 TB, compressed several times in columnar storage.

3.5 Summary

QuantityEstimateDesign consequence
Location updates250 000/sStateless ingest, partitioned stream, in-memory latest state
Requests~700/s global peak, ~56/s in largest cityDispatch per city shard is small and fast
ETA~10 000/s incl. previewsDedicated routing service with precomputed graph
History~1 TB/dayColumnar store with TTL; access controlled

4. API design

4.1 Endpoints and channels

ChannelOperationPurposeIdempotent
Driver stream (WebSocket or gRPC stream)locationPosition, heading, speed, timestampLast-write-wins by timestamp
Driver streamoffer (server to driver), accept, declineTrip offersAccept deduplicated by offerId
HTTPS POST/v1/tripsRider requests a tripIdempotency-Key required
HTTPS GET/v1/estimates?pickup=&dropoff=Price and pickup ETA previewYes
HTTPS POST/v1/trips/{id}:cancelCancel by rider or driverYes
Rider streamtrip_updateDriver position and state during tripClient keeps latest

4.2 Request and response examples

A ride request carries the price quote the rider saw, so the server can honour it or ask for confirmation if it expired:

POST /v1/trips HTTP/1.1
Content-Type: application/json
Idempotency-Key: 3b1f6a0e-9a52-4c1e-8f3e-6c4d2a1b7e90

{"pickup": {"lat": 52.52003, "lng": 13.40495}, "dropoff": {"lat": 52.50754, "lng": 13.37603}, "product": "standard", "quoteId": "q_77Z1"}
HTTP/1.1 202 Accepted
Content-Type: application/json

{"tripId": "t_5HX9", "state": "matching", "quoteExpiresAt": "2026-03-14T19:05:00Z"}

4.3 Errors

StatusProblem typeWhen
409active-trip-existsRider already has an active trip
410quote-expiredQuote expired; client fetches a new estimate
422outside-service-areaPickup outside operating zones
503no-supplyNo drivers available; retry suggested after N seconds

5. Data model

5.1 Entities

Driver supply state is hot and in memory; trips are durable records with a strict lifecycle.

5.2 Storage choices

DataStorePartition keyMain queriesWhy
Driver state and cell indexIn-memory store per city shard (replicated)city_id, then h3_cellDrivers in a set of cells with status availableMicrosecond lookups; rebuilt from stream within seconds
Trips and offersRelational database sharded by city_idtrip_idGet trip; active trip by rider or driverTransactions for state changes
Location streamPartitioned logdriver_idReplay to rebuild state; history sinkDurable buffer
Location historyColumnar store with 30-day TTLDay, cityTrip reconstruction, safety investigationsCheap scans, access-controlled
Road graph and trafficETA service memory, rebuilt daily with live speedsRegionShortest-time pathsPrecomputed for speed

6. High-level design

Each city (or large metro region) is served by its own dispatch shard; only ingestion, ETA and storage are shared services.

ComponentResponsibilityScales by
Location gatewayAuthenticate drivers, validate and timestamp updates, publishStateless replicas
Location serviceMaintain latest state and H3 index per cityCity shards
DispatchCollect requests, find candidates, batch-match, issue offersCity shards (single writer per shard)
ETA serviceRoad-network travel time with live trafficReplicas per region
Trip serviceTrip state machine, cancellations, notificationsStateless, DB sharded by city
Pricing aggregatorSupply and demand per cell, multipliersStream partitions
[Component][Responsibility][Scaling unit]

7. Detailed design

7.1 Location ingestion

Updates carry a device timestamp and a server receive timestamp; the location service applies an update only if it is newer than the stored one (last-write-wins by device time, rejected if more than 30 s in the future). Out-of-order and duplicate updates are therefore harmless. Implausible jumps (speed above 200 km/h between fixes) are flagged for anti-fraud and not used for matching.

7.2 Geospatial index

OptionCell shapeNeighbour searchNotes
GeohashRectangles, varying size by latitude8 neighbours plus edge cases at cell boundariesSimple, string prefix friendly
S2Quadrilaterals on a cube projectionCell coveringsGood for arbitrary regions
H3Hexagons (with 12 pentagons globally)gridDisk(k): all neighbours are equidistantUniform neighbour distance suits radius search and pricing

The design uses H3 resolution 9 (average cell area about 0.1 km², edge about 175 m) for driver lookup and resolution 7 (about 5 km²) for pricing. A candidate search starts with gridDisk(origin, 2) (19 cells, roughly a 0.8 km radius), widening to k=4k = 4 (61 cells) and k=8k = 8 (217 cells) if fewer than 10 available drivers are found. The number of cells in a disk of radius kk is 3k(k+1)+13k(k+1) + 1.

7.3 Matching algorithm

Greedy "nearest available driver per request" is simple but globally wasteful when requests cluster. Instead, dispatch collects requests for a window of w=2w = 2 s per shard and solves an assignment problem:

min⁡∑i∈R∑j∈Dxij ETAijs.t.∑jxij≤1,  ∑ixij≤1,  xij∈{0,1}\min \sum_{i \in R} \sum_{j \in D} x_{ij}\, \text{ETA}_{ij} \quad \text{s.t.} \quad \sum_{j} x_{ij} \le 1,\ \ \sum_{i} x_{ij} \le 1,\ \ x_{ij} \in \{0, 1\}

with a penalty for leaving a request unmatched. For a window with tens of requests and hundreds of candidates, the Hungarian algorithm or a min-cost flow solves this in milliseconds. Candidate edges are pruned to each request's 10 nearest drivers by straight-line distance before ETA is computed. Under overload (window larger than 200 requests) the shard falls back to greedy matching. Other factors (driver acceptance probability, vehicle class, fairness of trip distribution among drivers) enter as cost adjustments agreed with operations.

7.4 Request to assignment (critical path)

The rider gets an assigned driver after one batch window and the driver's acceptance.

7.5 Consistency of driver assignment

  • Invariant: a driver is in at most one of offered or on_trip for at most one trip at any time.
  • Single writer: one dispatch process owns a city shard at a time, elected through a lease with a fencing token; it is the only writer of driver status in that shard.
  • Compare-and-set: status changes are conditional on the stored version, so even a stale former leader (old fencing token) cannot overwrite. Offers expire after 15 s; an expiry reverts offered to available unless an accept arrived first.
  • Durable outcome: the accepted assignment is written to the trips database transactionally (trip state and driver ID together); the in-memory index is a cache of supply state that can be rebuilt from the stream and the trips database after failover.
  • Cross-shard edges: a driver near a city boundary belongs to the shard of their current cell; shards hand a driver over on cell change through the location stream, never by both shards writing.

7.6 Trip state machine

Trip states are persisted; every transition is validated against this diagram, and both apps render from it.

7.7 ETA service

Travel times come from a road graph with precomputed shortcuts (for example contraction hierarchies) and live segment speeds from recent trips, updated every minute. A learned correction model adjusts the route-based estimate for pickup-specific delays such as entering a station forecourt. ETAs used for matching are cached per (driver cell, pickup cell) pair for 30 s.

7.8 Surge pricing note

Every minute, the pricing aggregator computes per resolution-7 cell the ratio of open requests to idle drivers, smooths it over neighbouring cells and time, and maps it to a multiplier through a capped, monotonic curve. Riders see the price before confirming (the quote ID in 4.2 locks it for a few minutes). Pricing rules are subject to local regulation (caps during emergencies, disclosure requirements) and must be reviewed by legal and policy teams per market.

8. Scalability and performance

ConcernApproachLimit before redesign
Location updatesPartitioned stream; per-city consumersExample: ~50 000 updates/s per city shard
Large metrosSplit a metro into several shards by H3 resolution-5 parent cellsHand-off overhead at shard boundaries
ETA loadCache by cell pair; precomputed graph~20 000 queries/s per region cluster
Rider tracking fan-outOnly riders with an active trip subscribe to their driver[Limit]
Batch solverWindow and candidate pruningGreedy fallback above 200 requests per window

Latency budget from request to offer sent (target p95 3 s):

StepBudget
Trip creation100 ms
Waiting for batch window (average half of 2 s)1 000 ms
Candidate lookup10 ms
ETA calls150 ms
Solve and CAS50 ms
Offer delivery to driver app300 ms
Headroom1 390 ms

9. Reliability, security and operations

9.1 Failure modes

FailureImpactDetectionMitigation
Dispatch shard leader diesMatching pauses in that cityLease lossStandby takes over within 5 s; rebuilds state from stream and trips DB
Location stream lagStale positions, poor matchesConsumer lag, position age at matchExclude drivers with position older than 10 s; alert
ETA service downPoor rankingError rateFallback to straight-line distance x city speed factor
Driver app offline after acceptRider waitsMissed heartbeats on tripAfter 60 s without heartbeat, offer rematch to rider
Trips DB shard downRequests fail in a cityError rateSynchronous standby; requests rejected with clear message, not silently queued
[Failure][Impact][Signal][Mitigation]

9.2 Security, privacy and abuse

ThreatControl
GPS spoofing by driversPlausibility checks, device attestation, anomaly models
Exposure of rider and driver locationsPrecise driver location shown only to the matched rider during the trip; nearby-car display uses jittered positions
Location history misuseRole-based access, purpose logging, 30-day raw retention, de-identified aggregates afterwards
Fake requests to manipulate surgeAccount trust scores; requests without confirmed payment method excluded from demand
[Threat][Control]

9.3 Observability

SignalMetric or logAlert threshold
Time to matchRequest to accept p95 per cityAbove 30 s for 10 min
Unmatched rateno_drivers / requestsAbove 5% (marketplace alert)
InvariantDrivers with more than one active offer or tripAny
Position ageAge at match time p99Above 10 s
ETA errorPredicted vs. actual pickupMedian above 1.5 min

9.4 Cost estimate

ItemUnit costQuantityMonthly cost
Ingestion and stream[$/node-month][n][$]
Dispatch shards (leader + standby per city)[$/node-month]Example: 2 x 300 cities, small nodes[$]
ETA service[$/node-month][n][$]
Location history storage[$/TB-month]Example: ~32 TB raw, less after compression[$]
Map data licence[$/month][Licence][$]
Total[$]

10. Trade-offs and alternatives

DecisionOptionsChosenWhyWhat we give up
Spatial indexGeohash; S2; H3; database geo indexH3Equidistant neighbours, same cells for pricing and supplyHexagons do not nest exactly; pentagon edge cases
MatchingGreedy nearest; batched assignment; driver broadcast (first to accept wins)Batched assignmentLower total pickup time, fair to driversUp to 2 s added wait
Assignment safetyDistributed lock service; single writer per shard + CASSingle writer + CASNo extra lock round trip, simple reasoningShard failover pause of a few seconds
Driver state storeDatabase; in-memory per shardIn-memory, rebuilt from streamSpeed at 250 000 updates/sRebuild needed after failover
Distance metricStraight line; road ETARoad ETA for top candidatesAvoids "across the river" matchesETA service cost and dependency
[Decision][Options][Chosen][Why][Cost]

11. Delivery plan and risks

11.1 Milestones

MilestoneScopeExit criterionTarget date
M1Location pipeline and H3 index in one city (shadow)Position age p99 under 10 sYYYY-MM-DD
M2Greedy dispatch with CAS assignment in one cityZero invariant violations for 30 daysYYYY-MM-DD
M3Batched matchingPickup ETA down 10% in A/B by cityYYYY-MM-DD
M4All cities, surge v2Per-market legal sign-offYYYY-MM-DD

11.2 Rollout and rollback

[Example: Switch city by city during off-peak hours, with the old dispatch kept warm in shadow mode. Rollback switches the city's request routing back; active trips continue in the trip service, which both dispatchers share.]

11.3 Risks

IDRiskLikelihoodImpactMitigationOwner
R-01Example: Batch window increases perceived waitMediumMediumAdaptive window (0.5-2 s) by demand[@name]
R-02Example: Shard failover during peak causes matching gapLowHighHot standby consuming the same stream[@name]
R-03Example: Surge rules breach local regulationMediumHighPer-market caps and legal review[@name]
[R-nn][Risk][L / M / H][L / M / H][Mitigation][@name]

12. Appendices

12.1 Open questions

IDQuestionOwnerDue
OQ-01Example: Do we let drivers see destination before accepting?[@name]YYYY-MM-DD
OQ-02Example: Airport queues: first-in-first-out or matching?[@name]YYYY-MM-DD
[OQ-nn][Question][@name]YYYY-MM-DD

12.2 Glossary

TermMeaning
H3Hierarchical hexagonal geospatial index
gridDisk(k)All H3 cells within k steps of a cell
CASCompare-and-set: update only if the stored version matches
OfferTime-limited proposal of a trip to one driver
Surge multiplierPrice factor applied in an area when demand exceeds supply
[Term][Meaning]

12.3 References

ReferenceUse
H3 documentation (h3geo.org)Cell resolutions and grid functions in 7.2
Kuhn, "The Hungarian Method for the Assignment Problem" (1955)Assignment algorithm in 7.3
Geisberger et al., "Contraction Hierarchies" (2008)Routing in 7.7
[Local transport regulations per market]Pricing constraints in 7.8

Standard and licence note

Based on common industry practice for ride-hailing dispatch design and on the public references in 12.3. This is a reference design to adapt, with illustrative numbers; pricing and data-retention sections are a format, not legal advice. Original text written for this collection. Template licence: MIT.

Templat terkait