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
Mode d'emploi: 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.
Aperçu
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
| Field | Value |
|---|---|
| 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 updated | YYYY-MM-DD |
| Related | [Marketplace PRD], [Pricing policy], [Privacy impact assessment] |
Approvals
| Role | Name | Decision | Date |
|---|---|---|---|
| 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
| Version | Date | Author | Change summary |
|---|---|---|---|
| 0.1 | YYYY-MM-DD | [@name] | First draft for design review |
| 0.2 | YYYY-MM-DD | [@name] | Example: Batched matching every 2 s instead of greedy nearest driver |
| 0.3 | YYYY-MM-DD | [@name] | Example: City-sharded dispatch with single writer per city cell group |
| 1.0 | YYYY-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
| ID | Question | Recommended option | Section |
|---|---|---|---|
| Q-01 | Example: Geohash, S2 or H3? | H3 at resolution 9 for supply, 7 for pricing | 7.2, 10 |
| Q-02 | Example: Greedy or batched matching? | Batched every 2 s, greedy fallback under load | 7.3 |
| Q-03 | Example: How is double assignment prevented? | Single writer per city shard + CAS on driver state with offer TTL | 7.5 |
| Q-04 | Example: Location history retention? | 30 days raw, then aggregated and de-identified | 9.2 |
| [Q-nn] | [Question] | [Option] | [Section] |
2. Requirements
2.1 Functional requirements
| ID | Requirement | Priority |
|---|---|---|
| FR-01 | Example: Drivers go online and offline and share location while online | Must |
| FR-02 | Example: Riders see nearby drivers and an ETA before requesting | Must |
| FR-03 | Example: Request a ride; get a driver offer accepted within 30 s or a clear "no drivers" | Must |
| FR-04 | Example: Drivers accept or decline offers within 15 s | Must |
| FR-05 | Example: Live trip tracking for rider and driver | Must |
| FR-06 | Example: Cancellation by either side with defined fees | Must |
| FR-07 | Example: Dynamic pricing per area based on supply and demand | Should |
| [FR-nn] | [Requirement] | [Must / Should / Could] |
2.2 Non-functional requirements
| ID | Quality | Target | Measured as |
|---|---|---|---|
| NFR-01 | Matching latency | Example: Request to first offer p95 at or below 3 s | Dispatch metrics |
| NFR-02 | Correctness | Example: Zero drivers holding two active offers or trips | Invariant checker |
| NFR-03 | Location freshness | Example: Driver position used for matching at most 10 s old | Age at match time |
| NFR-04 | Availability | Example: 99.99% for ride requests per city | Synthetic requests |
| NFR-05 | ETA accuracy | Example: Median absolute error at or below 1 min for pickups under 10 min | Predicted 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
| Input | Symbol | Value | Source |
|---|---|---|---|
| Online drivers at global peak | Example: 1 million | Supply data | |
| Location update interval | Example: 4 s | App setting | |
| Trips per day | Example: 20 million | Demand data | |
| Peak to average factor | Example: 3 | Evening peak | |
| Candidates scored per request | Example: 10 | 7.3 | |
| Location record (ID, lat, lng, heading, speed, time, state) | Example: 50 bytes | Schema | |
| Largest city share of global peak | Example: 8% | Market data |
3.2 Location ingestion
Latest-position state in memory: B (with index overhead) 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
plus pre-request ETA previews (Example: 5 per request) at about 3 500/s. In the largest city, requests/s.
3.4 Location history
Thirty days of raw history is at most about 32 TB, compressed several times in columnar storage.
3.5 Summary
| Quantity | Estimate | Design consequence |
|---|---|---|
| Location updates | 250 000/s | Stateless ingest, partitioned stream, in-memory latest state |
| Requests | ~700/s global peak, ~56/s in largest city | Dispatch per city shard is small and fast |
| ETA | ~10 000/s incl. previews | Dedicated routing service with precomputed graph |
| History | ~1 TB/day | Columnar store with TTL; access controlled |
4. API design
4.1 Endpoints and channels
| Channel | Operation | Purpose | Idempotent |
|---|---|---|---|
| Driver stream (WebSocket or gRPC stream) | location | Position, heading, speed, timestamp | Last-write-wins by timestamp |
| Driver stream | offer (server to driver), accept, decline | Trip offers | Accept deduplicated by offerId |
| HTTPS POST | /v1/trips | Rider requests a trip | Idempotency-Key required |
| HTTPS GET | /v1/estimates?pickup=&dropoff= | Price and pickup ETA preview | Yes |
| HTTPS POST | /v1/trips/{id}:cancel | Cancel by rider or driver | Yes |
| Rider stream | trip_update | Driver position and state during trip | Client 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
| Status | Problem type | When |
|---|---|---|
| 409 | active-trip-exists | Rider already has an active trip |
| 410 | quote-expired | Quote expired; client fetches a new estimate |
| 422 | outside-service-area | Pickup outside operating zones |
| 503 | no-supply | No 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
| Data | Store | Partition key | Main queries | Why |
|---|---|---|---|---|
| Driver state and cell index | In-memory store per city shard (replicated) | city_id, then h3_cell | Drivers in a set of cells with status available | Microsecond lookups; rebuilt from stream within seconds |
| Trips and offers | Relational database sharded by city_id | trip_id | Get trip; active trip by rider or driver | Transactions for state changes |
| Location stream | Partitioned log | driver_id | Replay to rebuild state; history sink | Durable buffer |
| Location history | Columnar store with 30-day TTL | Day, city | Trip reconstruction, safety investigations | Cheap scans, access-controlled |
| Road graph and traffic | ETA service memory, rebuilt daily with live speeds | Region | Shortest-time paths | Precomputed 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.
| Component | Responsibility | Scales by |
|---|---|---|
| Location gateway | Authenticate drivers, validate and timestamp updates, publish | Stateless replicas |
| Location service | Maintain latest state and H3 index per city | City shards |
| Dispatch | Collect requests, find candidates, batch-match, issue offers | City shards (single writer per shard) |
| ETA service | Road-network travel time with live traffic | Replicas per region |
| Trip service | Trip state machine, cancellations, notifications | Stateless, DB sharded by city |
| Pricing aggregator | Supply and demand per cell, multipliers | Stream 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
| Option | Cell shape | Neighbour search | Notes |
|---|---|---|---|
| Geohash | Rectangles, varying size by latitude | 8 neighbours plus edge cases at cell boundaries | Simple, string prefix friendly |
| S2 | Quadrilaterals on a cube projection | Cell coverings | Good for arbitrary regions |
| H3 | Hexagons (with 12 pentagons globally) | gridDisk(k): all neighbours are equidistant | Uniform 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 (61 cells) and (217 cells) if fewer
than 10 available drivers are found. The number of cells in a disk of radius
is .
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 s per shard and solves an assignment problem:
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
offeredoron_tripfor 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 revertsofferedtoavailableunless 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
| Concern | Approach | Limit before redesign |
|---|---|---|
| Location updates | Partitioned stream; per-city consumers | Example: ~50 000 updates/s per city shard |
| Large metros | Split a metro into several shards by H3 resolution-5 parent cells | Hand-off overhead at shard boundaries |
| ETA load | Cache by cell pair; precomputed graph | ~20 000 queries/s per region cluster |
| Rider tracking fan-out | Only riders with an active trip subscribe to their driver | [Limit] |
| Batch solver | Window and candidate pruning | Greedy fallback above 200 requests per window |
Latency budget from request to offer sent (target p95 3 s):
| Step | Budget |
|---|---|
| Trip creation | 100 ms |
| Waiting for batch window (average half of 2 s) | 1 000 ms |
| Candidate lookup | 10 ms |
| ETA calls | 150 ms |
| Solve and CAS | 50 ms |
| Offer delivery to driver app | 300 ms |
| Headroom | 1 390 ms |
9. Reliability, security and operations
9.1 Failure modes
| Failure | Impact | Detection | Mitigation |
|---|---|---|---|
| Dispatch shard leader dies | Matching pauses in that city | Lease loss | Standby takes over within 5 s; rebuilds state from stream and trips DB |
| Location stream lag | Stale positions, poor matches | Consumer lag, position age at match | Exclude drivers with position older than 10 s; alert |
| ETA service down | Poor ranking | Error rate | Fallback to straight-line distance x city speed factor |
| Driver app offline after accept | Rider waits | Missed heartbeats on trip | After 60 s without heartbeat, offer rematch to rider |
| Trips DB shard down | Requests fail in a city | Error rate | Synchronous standby; requests rejected with clear message, not silently queued |
| [Failure] | [Impact] | [Signal] | [Mitigation] |
9.2 Security, privacy and abuse
| Threat | Control |
|---|---|
| GPS spoofing by drivers | Plausibility checks, device attestation, anomaly models |
| Exposure of rider and driver locations | Precise driver location shown only to the matched rider during the trip; nearby-car display uses jittered positions |
| Location history misuse | Role-based access, purpose logging, 30-day raw retention, de-identified aggregates afterwards |
| Fake requests to manipulate surge | Account trust scores; requests without confirmed payment method excluded from demand |
| [Threat] | [Control] |
9.3 Observability
| Signal | Metric or log | Alert threshold |
|---|---|---|
| Time to match | Request to accept p95 per city | Above 30 s for 10 min |
| Unmatched rate | no_drivers / requests | Above 5% (marketplace alert) |
| Invariant | Drivers with more than one active offer or trip | Any |
| Position age | Age at match time p99 | Above 10 s |
| ETA error | Predicted vs. actual pickup | Median above 1.5 min |
9.4 Cost estimate
| Item | Unit cost | Quantity | Monthly 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
| Decision | Options | Chosen | Why | What we give up |
|---|---|---|---|---|
| Spatial index | Geohash; S2; H3; database geo index | H3 | Equidistant neighbours, same cells for pricing and supply | Hexagons do not nest exactly; pentagon edge cases |
| Matching | Greedy nearest; batched assignment; driver broadcast (first to accept wins) | Batched assignment | Lower total pickup time, fair to drivers | Up to 2 s added wait |
| Assignment safety | Distributed lock service; single writer per shard + CAS | Single writer + CAS | No extra lock round trip, simple reasoning | Shard failover pause of a few seconds |
| Driver state store | Database; in-memory per shard | In-memory, rebuilt from stream | Speed at 250 000 updates/s | Rebuild needed after failover |
| Distance metric | Straight line; road ETA | Road ETA for top candidates | Avoids "across the river" matches | ETA service cost and dependency |
| [Decision] | [Options] | [Chosen] | [Why] | [Cost] |
11. Delivery plan and risks
11.1 Milestones
| Milestone | Scope | Exit criterion | Target date |
|---|---|---|---|
| M1 | Location pipeline and H3 index in one city (shadow) | Position age p99 under 10 s | YYYY-MM-DD |
| M2 | Greedy dispatch with CAS assignment in one city | Zero invariant violations for 30 days | YYYY-MM-DD |
| M3 | Batched matching | Pickup ETA down 10% in A/B by city | YYYY-MM-DD |
| M4 | All cities, surge v2 | Per-market legal sign-off | YYYY-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
| ID | Risk | Likelihood | Impact | Mitigation | Owner |
|---|---|---|---|---|---|
| R-01 | Example: Batch window increases perceived wait | Medium | Medium | Adaptive window (0.5-2 s) by demand | [@name] |
| R-02 | Example: Shard failover during peak causes matching gap | Low | High | Hot standby consuming the same stream | [@name] |
| R-03 | Example: Surge rules breach local regulation | Medium | High | Per-market caps and legal review | [@name] |
| [R-nn] | [Risk] | [L / M / H] | [L / M / H] | [Mitigation] | [@name] |
12. Appendices
12.1 Open questions
| ID | Question | Owner | Due |
|---|---|---|---|
| OQ-01 | Example: Do we let drivers see destination before accepting? | [@name] | YYYY-MM-DD |
| OQ-02 | Example: Airport queues: first-in-first-out or matching? | [@name] | YYYY-MM-DD |
| [OQ-nn] | [Question] | [@name] | YYYY-MM-DD |
12.2 Glossary
| Term | Meaning |
|---|---|
| H3 | Hierarchical hexagonal geospatial index |
gridDisk(k) | All H3 cells within k steps of a cell |
| CAS | Compare-and-set: update only if the stored version matches |
| Offer | Time-limited proposal of a trip to one driver |
| Surge multiplier | Price factor applied in an area when demand exceeds supply |
| [Term] | [Meaning] |
12.3 References
| Reference | Use |
|---|---|
| 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.
Modèles associés
Database Design Document Template (ERD and Physical Model)
Database design: DBMS choice, conceptual, logical and physical models, indexes, partitioning, integrity, security, backup, capacity
Architecture & Conception systèmeChat System Design Template (Reference Design)
Real-time 1:1 and group chat: WebSocket gateways, per-conversation ordering, fan-out, receipts, offline sync, push, E2EE
Architecture & Conception systèmeCollaborative Editing System Design Template (Reference Design)
Real-time co-editing: OT vs CRDT, sequence CRDT, state-vector sync, presence, offline merge, snapshots and compaction, access