Design a Food Delivery Tracking System
Design a Zomato-style food delivery tracker step by step: order correctness, rider dispatch, GPS ingestion, live updates, failures, and the reasoning behind each decision.
Page content
Alice orders dinner from a nearby restaurant. The kitchen accepts the order. A rider, Dev, is assigned and travels to the restaurant, picks up the food, and heads toward Alice.
Alice sees two kinds of change on her screen:
Order status Restaurant accepted → preparing → picked up → delivered
Rider location A new point on the map every few seconds
They may look like one feature, but they have very different correctness requirements. Losing an intermediate GPS point is harmless because another arrives soon. Losing the DELIVERED transition is not.
That difference is the center of this design:
How do we keep order state correct while moving a much larger stream of temporary GPS updates to customers in near real time?
We will build the system around four concerns: the order, dispatch, location, and live client delivery.
1. Clarify the problem
“Design Zomato” could include restaurant search, menus, checkout, payment, recommendations, dispatch, maps, chat, and support. That is too broad for one interview.
I would ask:
- Are we designing the complete marketplace or the flow after checkout?
- Must we design rider assignment?
- How fresh should the customer map be?
- Can a rider carry more than one order?
- Do we need an exact route and ETA?
- How many cities and daily orders should we support?
- What should happen when the rider loses connectivity?
If the interviewer provides no extra constraints, I would state:
Scope Place order, accept, dispatch, track, deliver
Catalog/search Out of scope
Payment Authorization is included; payment internals are not
Map freshness Usually within 3–5 seconds
Rider capacity One active delivery in the core design
ETA Required, but approximate
Chat Out of scope
Deployment Many cities; each order belongs to one city
Real platforms batch compatible deliveries. Starting with one active delivery per rider keeps assignment correctness understandable; batching can be added later by replacing “free/busy” with capacity and route constraints.
2. Functional requirements
The core system must:
- Let Alice place an order with one restaurant.
- Let the restaurant accept or reject it.
- Match the order to an available nearby rider.
- Let the restaurant and rider advance the order through valid states.
- Show Alice the current order state.
- Show Alice the rider’s latest location and ETA during delivery.
- Recover when Alice closes the app and reconnects later.
- Support cancellation and stop tracking after the trip ends.
The first version does not include:
- restaurant discovery and menu search;
- offers, ads, and recommendations;
- multi-restaurant carts;
- customer-rider chat;
- route optimization for batched deliveries; or
- a full analytics warehouse.
3. Non-functional requirements
The requirements differ by data type.
| Requirement | Target |
|---|---|
| Place-order latency | p99 below 500 ms, excluding slow payment-provider time |
| Status delivery | Usually visible within 1–2 seconds |
| Map freshness | Latest valid location within 3–5 seconds |
| Order durability | Never lose an acknowledged order |
| Assignment correctness | One order has at most one active rider |
| Order consistency | Every state transition follows the state machine |
| Location consistency | Newest valid point wins; duplicates may be dropped |
| Availability | Live tracking degrades gracefully during partial failures |
| Retention | Orders for months/years; raw GPS only for days or weeks |
“Eventually consistent” is too vague. We should say exactly where delay is acceptable:
Order/payment record durable before success
Order state transition serialized per order
Rider assignment atomic winner
Customer status screen may lag by 1–2 seconds
Latest GPS point may lag or lose one event
ETA derived and approximate
Analytics may lag by minutes
Privacy is also a correctness requirement. A user who guesses an order_id must not be able to watch another rider or customer.
Important edge cases
- Alice retries checkout after her network times out.
- Payment succeeds but writing the order fails, or the reverse.
- The restaurant accepts just as Alice cancels.
- Two riders accept the same offer.
- One rider is assigned to two orders.
- Dev’s phone sends duplicate and out-of-order locations.
- Dev enters a tunnel and stops reporting.
- Alice reconnects while
PICKED_UPoccurs. - A slow customer connection cannot consume every location event.
- A lunch spike overloads one city but not the rest of the country.
4. Estimate the scale
Use round numbers that expose the high-frequency path.
Assume:
Orders per day 1 million
Orders in peak hour 15% = 150,000
Average tracked trip duration 30 minutes
Online riders during peak 120,000
Active riders during peak 75,000
Active location interval 3 seconds
Idle location interval 10 seconds
Order traffic
Peak-hour creates
= 150,000 / 3,600
≈ 42 orders/second
Allow a 3× burst
≈ 125 orders/second
Each order may have six or seven state changes, so order-state writes are still only hundreds per second. This is a transaction and correctness problem, not a raw write-throughput problem.
Concurrent tracked orders
150,000 starts/hour × 0.5-hour duration
≈ 75,000 concurrent trips
Those trips create roughly 75,000 customer tracking sessions, plus restaurant and rider connections. A regional fleet may hold around 100,000–200,000 concurrent long-lived connections at peak.
Location ingestion
Active riders report frequently; idle riders report less frequently because dispatch still needs to know where they are.
75,000 active / 3 seconds ≈ 25,000 events/second
45,000 idle / 10 seconds ≈ 4,500 events/second
Total ≈ 30,000 events/second
This corrects an easy mistake: riders must send location before assignment, otherwise the dispatch service cannot find nearby riders.
At roughly 200 bytes after metadata:
30,000 × 200 bytes
≈ 6 MB/second
≈ 520 GB/day of raw location events
That is why raw GPS has short retention or is sampled before long-term storage.
Outbound live updates
An active order usually has one or two customer devices watching:
25,000 active-trip locations/second
× 1–2 viewers
≈ 25,000–50,000 outbound events/second
This is not celebrity fan-out. It is a large number of tiny rooms, each with very few subscribers.
5. Model the delivery journey
The order follows an explicit state machine:
PENDING_PAYMENT
↓
PLACED
↓
RESTAURANT_ACCEPTED
↓
PREPARING
↓
READY_FOR_PICKUP
↓
PICKED_UP
↓
DELIVERED
REJECTED and CANCELLED are terminal branches from allowed states.
The transition rules matter:
Restaurant may accept, reject, mark preparing/ready
Assigned rider may mark picked up/delivered
Customer may cancel only under product-specific conditions
System may cancel after restaurant/dispatch timeout
Location has a different lifecycle:
Rider offline no location
Rider online/free slower updates for matching
Rider assigned frequent updates for tracking
Order completed stop sharing with Alice; rider may remain online
The order is the durable business record. Location is a changing observation about a rider.
6. APIs
Use HTTP for commands and snapshots. Use a long-lived channel for live updates.
Place an order
Request:
POST /v1/orders
Authorization: Bearer <customer-token>
Idempotency-Key: <uuid>
{
"restaurant_id": "r_88",
"items": [
{ "item_id": "m_12", "quantity": 1 }
],
"delivery_address_id": "addr_7",
"payment_method_id": "pm_3"
}
Response:
HTTP 201 Created
{
"order_id": "o_9f3a",
"status": "PLACED",
"version": 2
}
The idempotency key prevents a retry from creating and charging for two dinners.
Change order state
POST /v1/orders/{order_id}/accept
POST /v1/orders/{order_id}/reject
POST /v1/orders/{order_id}/ready
POST /v1/orders/{order_id}/picked-up
POST /v1/orders/{order_id}/delivered
POST /v1/orders/{order_id}/cancel
Each command carries the last observed version:
{
"expected_version": 6
}
A stale command receives 409 Conflict with the current order snapshot.
Send rider location
Location belongs to the rider, not to an order endpoint. Idle riders need to report too.
POST /v1/riders/me/location
Authorization: Bearer <rider-token>
{
"sequence": 18291,
"lat": 12.9750,
"lng": 77.6010,
"accuracy_m": 9,
"heading": 84,
"speed_mps": 6.2,
"recorded_at": "2026-08-14T13:04:11.203Z"
}
HTTP 202 Accepted
The service derives the active order_id from the rider’s assignment. A rider cannot claim an arbitrary order in the payload.
Read a snapshot
GET /v1/orders/{order_id}/tracking
Authorization: Bearer <customer-token>
{
"order_id": "o_9f3a",
"status": "PICKED_UP",
"order_version": 7,
"rider": {
"display_name": "Dev",
"lat": 12.9750,
"lng": 77.6010,
"location_sequence": 18291,
"recorded_at": "2026-08-14T13:04:11.203Z"
},
"eta_minutes": 14
}
This endpoint is the recovery path when a live connection drops.
7. Basic data model
Order
Order
-----
order_id
customer_id
restaurant_id
city_id
status
version
total
delivery_address_snapshot
created_at
updated_at
Store an address snapshot because the user’s saved address may change after checkout.
Order items
OrderItem
---------
order_id
item_id
item_name_snapshot
unit_price
quantity
Price and name are snapshots for the same reason: the menu may change.
Delivery assignment
Delivery
--------
order_id
rider_id
status
assigned_at
picked_up_at
delivered_at
The durable store enforces one active delivery per order. Under the one-order-per-rider assumption, it also enforces one active delivery per rider.
Order events
OrderEvent
----------
order_id
version
event_type
actor_id
occurred_at
The current Order row answers normal reads. The append-only event history supports audits, support investigations, and reconnect/replay of important status transitions.
Rider profile versus presence
Rider profile (durable) rider_id, city_id, vehicle, account state
Rider presence (ephemeral) online, free/busy, last_seen, latest location
Do not persist is_online as if it were permanent truth. A heartbeat with a TTL defines presence.
Data ownership
Source of truth Orders, payments, durable delivery assignments
Derived data ETA, geo index, status projections
Cache/state Latest location, rider presence, order snapshot
Durable events Order transitions and payment/assignment outcomes
Ephemeral events Intermediate location updates
Connections WebSocket sessions and subscriptions
8. Start with the simplest design
An MVP can use:
Postgres orders, items, assignments
Redis latest rider location
Polling Alice GETs the snapshot every 5 seconds
This is a good first implementation.
At 75,000 active customers:
75,000 / 5 seconds
= 15,000 tracking reads/second
Most polls return almost the same data. If we shorten the interval for a smoother map, reads rise further. Polling also delays a rare but important state such as DELIVERED by up to the interval.
Polling remains a useful fallback, but at target scale it is wasteful as the primary live channel.
9. Polling, SSE, or WebSockets?
Polling
Alice → GET snapshot every few seconds
Simple and robust, but wasteful and delayed.
Server-Sent Events
SSE keeps one HTTP response open and lets the server push one-way events.
Alice
← RESTAURANT_ACCEPTED
← location 18290
← location 18291
← PICKED_UP
This matches the customer screen well: Alice mostly receives.
WebSockets
WebSockets provide a bidirectional channel. They require explicit heartbeat, reconnect, idle-timeout, and slow-client handling, but mobile platforms commonly support them well.
For this design I would choose:
Commands HTTP
Rider GPS ingest HTTP batches or gRPC stream
Foreground live screen WebSocket
Background notification APNs / FCM
Fallback polling
SSE is a reasonable alternative for browser customers. The important design is not the protocol name; it is the snapshot, authorization, routing, and recovery around the connection.
WebSockets do not solve fan-out by themselves. They only provide pipes.
10. Avoid the reconnect race
A naïve flow is unsafe:
1. Read snapshot at version 7
2. PICKED_UP becomes version 8
3. Subscribe to live events
Alice never receives version 8.
Use subscribe, buffer, then snapshot:
1. Authenticate and subscribe to order:o_9f3a
2. Buffer incoming events at the gateway
3. Read snapshot: order version 7, location sequence 18291
4. Send snapshot to Alice
5. Send buffered events newer than those positions
6. Continue live
Alternatively, subscribe with after_order_version=7 to a replayable event stream. Durable order events can be replayed. Intermediate location points do not need replay because the snapshot includes the latest point.
Every live message includes its position:
Status event order_version
Location event rider_id + sequence
The client discards duplicates and older positions.
This is the actual reliability model. “Reconnect the WebSocket” alone is not enough.
11. High-level architecture
Customer app Restaurant app Rider app
│ │ │
└──────────┬───────┴──────────┬───────┘
▼ ▼
┌─────────────┐ ┌──────────────┐
│ API Gateway │ │Realtime Gate.│
└──┬───────┬──┘ │ WebSockets │
│ │ └──────┬───────┘
▼ ▼ │
Order Svc. Location Svc. │
│ │ │
┌─────▼───┐ ├──► Latest Location / Geo Index
│Order DB │ │ │
│+ Outbox │ └──► Location Pub/Sub ─────┤
└─────┬───┘ │
▼ │
Kafka ──► Dispatch Svc. ──► Rider offers
│ │
└──► Status event router ──────────┘
Why each component exists
- Order Service: owns the state machine and durable order record.
- Dispatch Service: finds candidates, manages offers, and commits one assignment.
- Location Service: validates frequent rider points and updates presence.
- Realtime Gateway: owns connections and sends only authorized events.
- Order database: source of truth for orders and assignments.
- Kafka: durably carries business events and absorbs consumer lag.
- Location pub/sub: routes replaceable, short-lived GPS updates with low latency.
- Redis or equivalent: stores latest location and the nearby-rider geo index.
Separating Order and Location protects correctness work from a high-volume sensor stream.
12. Place order and payment safely
The payment provider and Order database cannot share a local transaction. Use a saga, not a pretend distributed transaction.
Create PENDING_PAYMENT order
↓
Authorize payment with order_id as idempotency key
↓
Payment succeeds?
├── yes → transition to PLACED
└── no → transition to PAYMENT_FAILED
If payment succeeds but the response is lost, retry the provider request with the same idempotency key and retrieve the original authorization.
If an order cannot proceed after authorization, issue a compensating void/refund. Persist each step and its provider reference.
Every committed order transition writes an outbox row in the same database transaction:
BEGIN
update order version
insert order_event
insert outbox event
COMMIT
The outbox relay publishes to Kafka. Kafka being unavailable delays notifications and dispatch; it does not erase the accepted transition.
13. Enforce the order state machine
Do not accept arbitrary status strings.
Use a compare-and-set update:
UPDATE orders
SET status = 'PICKED_UP',
version = version + 1
WHERE order_id = 'o_9f3a'
AND status = 'READY_FOR_PICKUP'
AND version = 6;
If zero rows change, another command won or this transition is invalid.
This protects races such as:
Alice cancels at version 4
Restaurant accepts stale version 3
The stale accept fails. The API returns the current snapshot.
State commands are idempotent. Repeating the same successful command returns its result rather than advancing twice.
14. Dispatch nearby riders
Dispatch is a marketplace:
Order needs pickup at restaurant
↓
Find nearby online riders with capacity
↓
Rank candidates
↓
Send time-limited offer
↓
Commit exactly one assignment
Candidate retrieval
Maintain a geo index of online, apparently available riders:
city:blr:available
rider_dev → (12.9750, 77.6010)
rider_lee → (12.9690, 77.5900)
Redis GEO or H3/S2 cells can support “riders near the restaurant.” The pickup location is the restaurant, not Alice’s drop-off.
The geo index is derived and may be stale. It narrows candidates; it does not decide assignment.
Candidate ranking
Distance alone is weak. Rank using:
- predicted travel time to the restaurant;
- restaurant preparation time;
- rider capacity and active route;
- vehicle type;
- acceptance likelihood; and
- fairness/load balancing.
Dispatch may intentionally wait before assigning if food needs 25 minutes. Sending Dev immediately makes him wait outside and reduces marketplace capacity.
Offer strategy
Two common choices:
Sequential offer one rider, wait briefly, then next
Small batch offer 2–3 riders, first valid accept wins
Sequential offers reduce rejected work but may be slow. Small batches improve assignment latency but create loser notifications and more contention. Do not broadcast to thousands of riders.
Prevent double assignment
An offer is not an assignment. It is a short lease:
Offer { order_id, rider_id, offer_id, expires_at }
On accept, Dispatch performs one authoritative transaction:
order is still unassigned
AND rider has capacity
AND offer has not expired
↓
insert active assignment
update order to RIDER_ASSIGNED
Enforce uniqueness for both active order_id and active rider_id under the one-trip assumption. A unique order constraint alone prevents two riders on one order but does not prevent one rider from taking two orders.
15. Ingest rider location
Riders report while online, with adaptive frequency:
Offline none
Online and idle every 10–15 seconds
Assigned / moving every 2–5 seconds
App background / low battery slower, OS permitting
The Location Service:
- Authenticates the rider.
- Checks sequence, timestamp, and accuracy.
- Rejects impossible jumps or marks them low confidence.
- Updates the latest-location key if the event is newer.
- Updates the geo index if the rider is dispatchable.
- Publishes to the active order’s location channel when assigned.
- Optionally appends a sampled event to a short-retention stream.
rider:{rider_id}:location
lat
lng
accuracy
sequence
recorded_at
received_at
active_order_id
Ordering and duplicates
Use a monotonically increasing sequence generated by the rider app, plus timestamps:
incoming sequence <= stored sequence
→ duplicate or stale, ignore
incoming sequence > stored sequence
→ validate and replace
After app reinstall or sequence reset, issue a new session id. Compare (session_id, sequence), not sequence alone.
Device clocks are untrusted. Keep recorded_at for movement, received_at for operations, and reject timestamps too far in the future.
16. Keep GPS out of the order database
At 30,000 events per second, updating orders.last_lat would:
- contend with important status transitions;
- multiply database replication and backup traffic;
- make one sensor retry a business transaction;
- create noisy row updates with little retention value.
Use separate storage by purpose:
Order DB rare, durable, transactional state
Latest-location KV one replaceable point per rider
Geo index online/available riders by area
Short-retention log optional GPS history for ETA and support
If latest-location Redis is lost, rider pings refill it. If the Order database is lost, active deliveries cannot be reconstructed safely. The durability policies should reflect that difference.
17. Route events to the right connection
Each order is a small topic:
order:o_9f3a
├── Alice's phone
└── Alice's second device
A Realtime Gateway node holds many sockets and a local registry:
order_id → local connection ids
Flow:
Alice connects to gateway G7
↓
G7 authorizes and subscribes to order:o_9f3a
↓
Location/Status router publishes order:o_9f3a
↓
Only gateway nodes with local subscribers receive it
↓
G7 writes to Alice's socket
Use a regional subject-based broker such as NATS or Redis Pub/Sub for location routing. Use Kafka for durable order events.
Why two systems?
Order status must survive consumer restart and support replay
Location point is obsolete when the next point arrives
Kafka can carry both, but a consumer group does not by itself route one event to every gateway node holding a matching socket. A lightweight pub/sub layer makes per-order routing simpler. The added operational system is a trade-off; at smaller scale, one Redis Streams/Pub/Sub setup may be enough.
18. Handle backpressure
A slow phone cannot consume an unlimited queue.
Classify events:
Order status non-droppable, ordered by order_version
Latest location coalescible: keep only the newest unsent point
ETA coalescible
Typing/map hints droppable
If Alice’s socket is slow, replace the pending location with a newer one rather than queueing a trail of obsolete points.
Set per-connection queue limits. If a client remains behind, close the socket and force snapshot-based reconnect. Never let one mobile connection exhaust a gateway.
At city-wide overload, ask rider apps to report every five seconds instead of three. That cuts ingest by 40% while preserving useful tracking.
19. ETA is derived, not truth
ETA depends on:
- rider’s latest location;
- road route and live traffic;
- restaurant preparation estimate;
- pickup wait;
- building handoff time; and
- historical error correction.
Before pickup:
ETA = remaining preparation + rider travel to restaurant
+ delivery route + handoff
After pickup:
ETA = route travel time + handoff
Do not call an external routing provider on every GPS point. Recompute when:
- the rider moves a meaningful distance;
- the planned route changes;
- traffic data materially changes; or
- a periodic interval elapses.
Cache route results by road segment/geohash where useful. ETA may be stale by seconds; display a range rather than false precision.
20. Caching and presence
| Data | Example key | TTL / invalidation |
|---|---|---|
| Order snapshot | order:o_9f3a | Invalidate on order version change |
| Latest rider location | rider:dev:loc | Short TTL; overwrite with newer sequence |
| Rider presence | rider:dev:presence | Heartbeat TTL |
| Available-rider geo index | city:blr:available | Remove on assignment/offline |
| Restaurant state | restaurant:r_88:status | Short TTL + explicit update |
Presence is a lease:
heartbeat arrives extend TTL
TTL expires rider considered offline
The geo index is not authoritative. Dispatch confirms rider capacity in durable assignment state before committing.
Do not cache an authorized tracking response as a public object. Cache the underlying order snapshot and location, then authorize each caller.
21. Failure scenarios
Order database unavailable
Fail place, accept, assign, pickup, and delivery commands. Reads may use a replica, but do not accept transitions without a durable primary/quorum.
Payment provider is slow
Keep the order in PENDING_PAYMENT, apply a timeout, and retry with the same provider idempotency key. Do not block request threads indefinitely.
Kafka unavailable
Order transitions still commit with outbox rows. Dispatch and notifications lag until the relay publishes. Alert on oldest outbox age.
Latest-location store unavailable
Continue accepting pings into a bounded stream if possible. The map shows “location temporarily unavailable” or the last known point. Do not write GPS into the Order DB as an emergency fallback.
Geo index unavailable
Pause new offers or use a slower recent-presence store with strict admission control. Existing deliveries continue. A stale or broad SQL scan should not overload the durable order database.
Location pub/sub unavailable
Riders continue updating the latest point. Live maps pause. Polling the tracking snapshot at a longer interval gives degraded service until pub/sub recovers.
Realtime Gateway dies
Clients reconnect to another node. Subscribe-buffer-snapshot prevents gaps. Intermediate location events may be lost; current location and order version recover the screen.
Rider goes offline
When last_seen_at grows stale:
- show Alice “location last updated N minutes ago”;
- alert operations/restaurant if the threshold is large;
- do not auto-deliver;
- do not offer that rider another trip.
Dispatch consumers fall behind
Monitor unassigned-order age, not only queue lag. Scale workers, widen candidate radius gradually, and avoid flooding all riders at once.
22. Consistency and correctness
| Concern | Mechanism |
|---|---|
| Duplicate checkout | API idempotency key |
| Payment/order mismatch | Saga + provider idempotency + compensation |
| Invalid state transition | State machine + compare-and-set version |
| Duplicate Kafka event | Idempotent consumer keyed by event id/version |
| Two riders for one order | Unique active assignment by order_id |
| One rider on two orders | Unique active assignment by rider_id or capacity transaction |
| Stale GPS | (session_id, sequence) comparison |
| Missed reconnect event | Subscribe-buffer-snapshot or replay after version |
| Stale cache | Order version in cache + explicit invalidation |
Exactly-once delivery is not required. Durable events can arrive at least once because handlers are idempotent.
Order events are partitioned by order_id in Kafka to preserve per-order order. Location events are partitioned by rider_id. We do not need one global order across the city.
23. Security and privacy
Authentication and roles
Customers, restaurant staff, riders, and operations users have different permissions. The gateway authenticates; each service authorizes its own resource.
Tracking authorization
Only these principals may subscribe:
- the customer who owns the order;
- the assigned rider;
- authorized restaurant staff while relevant; and
- audited support roles.
Authorization is checked at subscription and periodically for long-lived connections. Delivery or cancellation revokes customer access to future rider location.
Location integrity
The rider token identifies the rider; the server derives the assignment. Validate impossible speed, GPS accuracy, rooted-device signals where appropriate, and repeated synthetic paths. Fraud detection is separate from the hot ingest decision.
Data minimization
Retain raw GPS only as long as operational, fraud, and legal needs require. Restrict who can query historical routes. Avoid putting customer PII into location events and logs.
Delivery confirmation
For high-risk orders, use a customer OTP or proof-of-delivery token. The rider cannot mark delivered without the server validating it.
24. Observability
Measure the user journey:
Order placed → restaurant accepted
Restaurant accepted → rider assigned
Rider assigned → picked up
Picked up → delivered
Core SLIs:
- assignment latency and percent unassigned after N minutes;
- order status propagation latency;
- customer location freshness (
now - recorded_at); - percent of active trips stale for more than 30 seconds;
- WebSocket connect success and unexpected disconnect rate;
- reconnect recovery success;
- outbox age and Kafka consumer lag;
- geo-index online count versus heartbeat count;
- optimistic-concurrency conflict rate;
- ETA absolute error at several trip stages.
Trace with order_id, rider_id, event id, order version, region, and connection id. Do not log precise GPS broadly.
Useful alerts are outcome-based:
- rising unassigned-order age;
- a city with stale locations;
- status propagation above the SLO;
- outbox backlog;
- a Tracking Gateway near connection/queue capacity;
- geo index and presence counts diverging.
25. Multi-city and multi-region
Food delivery is geographically local. Use city_id as the first routing boundary:
Global control plane
│
├── Bangalore cell
│ Order partition
│ Rider geo/presence
│ Dispatch workers
│ Realtime gateways
│
└── Mumbai cell
...
Alice’s Bangalore order should not share the Mumbai location stream or geo index.
Within a very large city, split by H3/S2 cells while allowing queries across neighboring cells. Rebalance boundaries carefully so a rider crossing a cell does not disappear from candidate searches.
An order stays owned by one home region for its lifetime. Replicate durable order state for disaster recovery. If the write region fails:
- serve last known snapshots from a replica;
- keep location display best-effort if regional services survive;
- stop taking new paid orders until a fenced writer is available;
- resume transitions only after promoting one authoritative region.
Availability is valuable, but accepting contradictory order transitions in two regions is worse than a short write outage.
26. Adding batched deliveries
Real riders may carry multiple compatible orders.
The simple boolean:
is_free
becomes:
capacity
active_stops
planned_route
pickup deadlines
food compatibility
Dispatch becomes a constrained insertion problem: can a new pickup/drop-off be inserted into Dev’s existing route without violating SLAs?
The architecture remains similar:
- geo index retrieves candidates;
- a route optimizer scores insertions;
- durable assignment transaction checks capacity/version;
- rider app receives an updated ordered stop list.
This is an extension, not a reason to complicate the first design.
27. Final architecture
ORDER CONTROL PLANE
───────────────────
HTTP commands
→ Order Service
→ Order DB + event history + outbox
→ Kafka (partitioned by order_id)
→ Dispatch / notifications / status router
LOCATION DATA PLANE
───────────────────
Rider GPS
→ Location Service
→ latest-location KV + rider geo/presence
→ regional location pub/sub
→ Realtime Gateways
CUSTOMER LIVE READ
──────────────────
Authorize + subscribe and buffer
→ fetch snapshot (order version + latest location sequence)
→ drain newer buffered events
→ continue over WebSocket
→ polling fallback
The reasoning chain is:
Order state is rare and correctness-critical
↓
GPS is frequent and replaceable
↓
Separate control and location paths
↓
Use a geo index to find candidates, but transact to assign
↓
Push live events through per-order topics
↓
Recover every connection from a versioned snapshot
28. Interview-ready summary
Key decisions to remember
- Scope the interview to order, dispatch, and tracking.
- Keep order state in a versioned, durable state machine.
- Treat payment/order coordination as a saga.
- Collect location from all online riders, not only assigned riders.
- Store latest GPS separately from the Order database.
- Use a geo index for candidates and a transaction for assignment truth.
- Use HTTP for commands and WebSockets/SSE for foreground live updates.
- Subscribe-buffer-snapshot to avoid reconnect gaps.
- Make status non-droppable and location coalescible under backpressure.
- Partition operations by city and durable event order by
order_id.
Likely interviewer follow-up questions
- Why WebSockets instead of SSE or polling?
- How does reconnect avoid missing an order transition?
- How do you prevent both double assignment and double booking?
- How do you coordinate payment with order creation?
- What happens if GPS arrives out of order?
- Why not store the latest location on the Order row?
- What happens when Redis, Kafka, or the pub/sub layer fails?
- How do you assign a rider before the food becomes cold?
- How would you support batched deliveries?
- How do you protect historical rider location?
Senior-level points that differentiate the answer
- Separate the correctness-critical control plane from the replaceable location data plane.
- Model online presence as a TTL lease, not a durable boolean.
- Explain the snapshot/subscription race and close it explicitly.
- Make the geo index a candidate source, never assignment truth.
- Enforce uniqueness by both order and rider under the single-trip assumption.
- Coalesce GPS for slow clients while preserving every status transition.
- Use different delivery semantics for Kafka status and ephemeral location pub/sub.
- Monitor assignment age and map freshness, not merely server CPU.
A 1–2 minute verbal answer
I would scope this to order placement, restaurant acceptance, rider dispatch, and live tracking. At one million orders per day, a peak hour may have about 75,000 concurrent trips. With 120,000 online riders reporting every three to ten seconds, location ingest is around 30,000 events per second, while durable order transitions are only hundreds per second. That tells me to separate the paths.
Orders live in a transactional, versioned state machine. Place-order uses an idempotency key and a payment saga; every transition writes an outbox event. Dispatch retrieves nearby riders from a city geo index, but a transaction checks the order is unassigned and the rider has capacity before committing, so the index is never assignment truth.
Location Service validates rider sequence numbers, overwrites a latest-location KV entry, updates presence/geo state, and publishes active-trip points to a regional per-order topic. Realtime Gateways hold WebSockets. To avoid a reconnect gap, the gateway subscribes and buffers first, fetches a snapshot with order version and location sequence, sends it, then drains newer events. Under backpressure I coalesce locations but never drop order status. The main SLIs are assignment time, status propagation, and customer-visible location freshness.
For the broader interview framework around this problem, see the System Design Interview Complete Guide.
