Geo & Location

Delivery / Logistics Tracking

Show a courier's live position and a fresh ETA: high-volume location pings absorbed by a stream, a geo-index for spatial queries, a smoothed derived view, and server push so the read path never touches the write path.

~30 min · intermediate

Problem & Requirements

Show a customer where their courier is and when the order will arrive, updated live. Couriers' phones emit GPS pings every few seconds; the customer's app displays a moving marker and a countdown. The two sides have wildly different rates — a city's worth of couriers produces a firehose of pings, while each customer wants a smooth marker and an ETA that doesn't flicker. The display can lag reality by a few seconds without anyone noticing, but it must never freeze or teleport.

That rate mismatch is the design. The write path (couriers pinging) and the read path (customers watching) must not touch each other directly, because coupling them makes the firehose drive synchronous work per viewer. A stream sits between them: pings append cheaply on one side, a processor turns them into a derived view on the other. A geo-index makes spatial questions — which courier is on this order, which couriers are near this restaurant — a cell lookup instead of a scan. The customer holds a server-push channel so updates arrive without polling, and the displayed state is eventually consistent: a smoothed view that trails the raw pings by a bounded few seconds. The spine is that nothing on the read path ever blocks on the ingest path.

Functional

  • report(courier, lat, lng) ingests a location ping; watch(order) opens a live channel for a customer.
  • Derive a current position and ETA per active order from the ping stream.
  • Answer spatial queries ("couriers near a point") for dispatch and association.

Non-functional (back-of-envelope)

QuantityTargetWhat it forces
Location pings~250k/s (1M couriers, one per 4 s)Append-only ingest, decoupled from reads through a stream
Concurrent watchers~100k–1MServer push, one channel per order, never polling
Display freshnessposition ≤ a few seconds staleA derived view updated by the stream; eventual consistency tolerated
Spatial query"couriers near X" in millisecondsGeo-index by cell, never a full scan
ETA recomputeoff the read path, per few secondsStream processor materializes ETA; reads serve a stored value
Ping reliabilitylossy, out-of-order, burstySmoothing and dead-reckoning; the input data is dirty
Push fan-outbounded per clientCoalesce updates so a slow client can't force a send per ping

The treacherous row is ping reliability. Volume is the easy axis — a stream absorbs 250k pings a second without drama. The killer is data quality: phones drop into tunnels, apps get backgrounded, GPS jitters, and pings arrive late and out of order. Showing the latest ping naively makes the marker jump and the ETA flicker, which reads as a broken app even though every component is "working." Taming that is build step six, and it is the difference between a tracker and a marker that teleports across the map.

Design

Six components, each tied to the principle it applies:

  1. Location ingest — a write endpoint that accepts pings at volume and appends them, doing no per-viewer work, so a courier's report returns immediately.
  2. Stream processor — consumes the ping stream and maintains per-order derived state (position, heading, ETA) asynchronously. This is stream processing, and it is what decouples the firehose from the viewers.
  3. Geo-index — active couriers indexed by grid cell (geohash, S2, or H3), so spatial queries resolve to a cell plus its neighbors. This is geo-indexing.
  4. ETA derivation — current position combined with a route and travel-time estimate, computed on the stream rather than recomputed on every read.
  5. Subscription and push fan-out — customers open a channel keyed by order, and the derived view is pushed as it changes. This is server push (WebSockets or SSE).
  6. Derived view with bounded staleness — a materialized "latest smoothed position + ETA" that reads serve and pushes carry, trailing the raw stream by seconds. This is eventual consistency, with smoothing to hide gaps and out-of-order pings.

The geo-index is best read as real open-source code: Uber's H3 hexagonal hierarchical grid and Google's S2 spherical geometry are the two libraries production systems actually use, and both publish the cell math the toy below sketches with geohash. At scale the two named systems line up along these components. Uber ingests location pings into Kafka, processes them in stream jobs, indexes geography with H3, and computes ETAs with routing plus learned models, pushing live updates to riders and eaters. DoorDash runs a comparable shape — a Kafka-based real-time event pipeline (Iguazu) processed in Flink, regional geo-sharding, and a dedicated ETA prediction service — and pushes Dasher location and ETA to the consumer app. They differ in the spatial library (H3 versus S2/geohash lineages) and in the stream framework, but both keep the consumer's read path on a derived, eventually-consistent view rather than on the raw ping stream, which is the decision this lesson builds toward.

Build it

1

Start with the obvious version: a courier POSTs its location into a row keyed by order, and the customer's app polls a status endpoint that reads the row and computes an ETA on the fly. It is correct and easy to follow. The failures stack up fast — polling is laggy and wasteful, the ETA is recomputed on every read, and there is no stream or spatial structure, so the firehose and the viewers share one synchronous path. The next step removes the polling with server push.

2

Let the customer open a long-lived channel — a WebSocket or an SSE stream — and have the server push an update when a ping arrives, keyed by order. This is server push: the customer no longer polls, updates arrive within a ping of reality, and idle viewers cost almost nothing. The failure it exposes is coupling — the ETA and any road-snapping now run inline on the ingest path, so an expensive computation or a slow consumer back-pressures the courier's report. Ingest and fan-out are welded together, which the next step pries apart.

3

Put a stream between ingest and processing. A ping append is now trivial — drop the event on the stream and return — while a separate processor consumes the stream, computes the ETA, updates derived state, and triggers the push. This is stream processing, and it is what lets ingest run at 250k/s regardless of how heavy ETA computation gets or how slow a consumer is; bursts buffer in the stream instead of blocking couriers. The remaining failure is spatial: questions like "which couriers are near this restaurant" or associating a stray ping with an order still scan every courier, which is O(N).

4

Index each courier by the grid cell it sits in so spatial queries become a cell lookup plus a ring of neighbors, not a scan of the fleet. A move updates two cells — remove from the old, add to the new — and "near X" unions the target cell with its eight neighbors. This is geo-indexing; geohash here stands in for the H3 or S2 cells Uber and DoorDash actually use. The failure left over is on the read side: customers are seeing whatever the latest ping says, and raw pings are jittery and arrive out of order, so the marker stutters and occasionally jumps backward.

5

Stop serving raw pings and materialize a per-order view that the processor maintains: the latest accepted position and ETA, with out-of-order pings dropped by event time. Reads and pushes both come from this view, which trails the raw stream by a bounded few seconds but never regresses. This is eventual consistency used deliberately — the display is allowed to be slightly behind in exchange for being smooth and monotonic. The failure that remains is the dirty-data row from the requirements: pings drop out in tunnels or when the app is backgrounded, so the marker freezes and then lurches, and slow clients still get a push per ping.

6

Hide the dirty input two ways. Between pings, interpolate the marker forward along the last known heading and speed (dead-reckoning) so it glides instead of waiting; past a staleness threshold, hold the last position and flag it as stale rather than guessing wildly. On the fan-out side, coalesce: send each channel at most one update per interval so a slow client gets the freshest snapshot, not a backlog of every ping. This is the robustness step the requirements flagged, keeping the eventually-consistent view smooth under lossy, bursty input while bounding per-client push cost.

Tradeoffs

DecisionWhat it buysWhat it costs
Stream processingIngest decoupled from reads; bursts buffer instead of blockingProcessing lag, and at-least-once delivery that needs dedup and ordering
Geo-indexing by cellSpatial queries in a cell lookup, not a fleet scanCell-boundary effects; precision is a tradeoff; re-index on every move
Server pushLive updates with no polling loadMany long-lived connections to hold open; reconnection and backfill logic
Derived view + eventual consistencySmooth, cheap reads; monotonic displayThe marker is always a few seconds behind reality
Dead-reckoning between pingsNo freeze-then-jump; a gliding markerThe shown position is an estimate and is sometimes wrong
Coalesced fan-outBounded per-client work under loadSlow clients see slightly older snapshots

Scaling it up

The single stream and index are the first thing to break. At a planet's scale the stream is partitioned and the geo-index sharded by region, so a node owns a set of cells rather than the world, and pings route to the shard that owns the courier's location. The hard part is hot regions — a city center at dinner concentrates load on a few cells — which forces dynamic rebalancing and a cell scheme whose resolution can adapt. Uber's H3 and the S2 cell hierarchy both exist partly to make this sharding tractable, with cells that nest cleanly across resolutions.

ETA is a prediction problem, not a ruler. A straight-line estimate is wrong the moment a one-way street or a traffic jam enters the picture, so production ETAs combine a routing engine over the road network with learned models on historical travel times, restaurant prep time, courier behavior, and current conditions. DoorDash and Uber both run dedicated prediction services for this, recomputing as the courier moves and as traffic shifts, which is far more than the _route_eta placeholder above and is its own prototype.

Holding a million push connections is a tier of its own. The connection layer is separated from the processing layer so that gateways terminate WebSocket/SSE connections, track which order each client watches, and subscribe to a pub/sub keyed by order, while the processors stay stateless about connections. Reconnection needs backfill — on reconnect a client should immediately get the latest snapshot rather than waiting for the next ping — and the tier has to fail over without dropping every viewer at once.

Event-time correctness is where the dirty-data row gets real. Pings are at-least-once, arrive out of order, and carry timestamps from clock-skewed phones, so the pipeline deduplicates, orders by event time with watermarks to bound how long it waits for stragglers, and decides what to do with data that arrives after its window closed. This is exactly the event-time and watermark machinery that stream frameworks like Flink provide, and DoorDash's use of Flink is largely about getting this right at volume.

Location is sensitive, so the trail is scoped and short-lived. Access to a courier's position is limited to the active order and the parties on it, sharing ends when the order is delivered, and raw trails are retained briefly rather than indefinitely. The live view also has to reconcile with the authoritative order state machine — assigned, picked up, en route, delivered — so the map and the order status never contradict each other. From here the concrete follow-ons are a geo-sharded ingest prototype that partitions the stream and index by H3/S2 cell with hot-region rebalancing, an ETA-prediction prototype pairing a routing engine with a learned travel-time model, and a connection-gateway prototype that builds the push tier with presence tracking and reconnect backfill. Each extends this stream-plus-derived-view core without rebuilding it.

References