System Design

Designing a Food Delivery System at Scale: DoorDash Architecture, Courier Dispatch & ETA Prediction

Building a food delivery platform that handles millions of orders per day — with real-time courier dispatch, sub-second ETA updates, and five-nines reliability — is one of the most demanding system design challenges in industry. This guide tears apart every subsystem: from idempotent order placement and H3-indexed geospatial dispatch, to ML-powered ETA prediction and dynamic surge pricing that keeps supply and demand in balance.

Md Sanwar Hossain April 7, 2026 21 min read Delivery Architecture
Food delivery system design DoorDash dispatch ETA courier matching architecture

TL;DR — Core Design Decisions

"Use an idempotent order service backed by a distributed saga, an H3 hex-grid dispatch engine with expanding ring search for courier matching, a Kafka-driven GPS pipeline for real-time location push, and a two-stage ML ETA model (prep time + travel time) that recalculates every 30 seconds. Partition every hot data path by geographic zone to contain blast radius and scale independently."

Table of Contents

  1. Functional & Non-Functional Requirements
  2. High-Level Architecture Overview
  3. Order Service — Cart, Checkout & Idempotent Payments
  4. Restaurant Service — Menu, Prep Time & Availability
  5. Courier Dispatch Engine — H3 Grid, Batching & Acceptance
  6. Real-Time Location Service — GPS Streams & WebSockets
  7. ETA Prediction — ML Features, Online Recalculation
  8. Dynamic Surge Pricing — Demand/Supply Ratio & Incentives
  9. Notification & Communication — Push, SMS, Rate Limiting
  10. Capacity Estimation — Peak Orders, GPS Events, Bandwidth
  11. Scalability & Reliability Patterns
  12. System Design Interview Checklist

1. Functional & Non-Functional Requirements

Before drawing boxes and arrows, nail down the exact problem scope. In a system design interview, requirements elicitation is the first thing the interviewer evaluates. Food delivery sits at the intersection of three real-time domains — ordering, logistics, and payments — all of which carry different SLA expectations.

Functional Requirements

Non-Functional Requirements

Dimension Target Notes
Availability 99.99% (order path) Multi-region active-active
Order placement latency < 500 ms p99 Async payment capture allowed
Dispatch offer latency < 3 s from order confirmed First courier offer sent
Location update frequency Every 5 seconds (courier app) Adaptive: 15 s when idle
ETA accuracy ±3 minutes (80th percentile) Customer-facing promise
Throughput 500,000 concurrent active orders Peak dinner rush globally
Consistency Eventual (location), strong (payments) Mixed model per domain

2. High-Level Architecture Overview

A food delivery platform is best understood as a set of loosely-coupled domain services communicating over an event bus, with geographic partitioning as the primary scaling axis. The following diagram illustrates the major components and their primary data flows.

Food delivery system design DoorDash dispatch ETA courier matching
Food Delivery Platform — high-level architecture showing Order Service, Dispatch Engine, Location Pipeline, and ETA Service. Source: mdsanwarhossain.me

The system is organized around six core domains, each owning its data store and communicating asynchronously through Kafka topics:

API Gateway & Client Communication

Customer and courier mobile apps communicate through a GraphQL API Gateway for request/response flows (menu queries, order placement, history). For real-time tracking, both apps maintain a persistent WebSocket connection to the Location Service through a dedicated WebSocket Gateway cluster. The WebSocket gateway is horizontally scaled and each instance maintains at most 50,000 concurrent connections, with sticky routing via a consistent hash on user_id.

3. Order Service — Cart, Checkout & Idempotent Payments

The order service is the most correctness-sensitive service in the stack. Duplicate charges, ghost orders, and lost state transitions are catastrophic — both financially and in customer trust. The architecture must guarantee exactly-once order creation even when mobile clients retry on network failures.

Idempotent Order Placement

Every POST /orders request from the client carries a client-generated idempotency_key (UUID v4). The server stores this key in a dedicated idempotency_keys table with the resulting order ID and response payload. On retry, the server looks up the key and returns the cached response — no downstream payment or database writes are executed.

-- Idempotency key table (PostgreSQL)
CREATE TABLE idempotency_keys (
    idempotency_key   UUID        PRIMARY KEY,
    order_id          BIGINT      NOT NULL,
    response_status   SMALLINT    NOT NULL,
    response_body     JSONB       NOT NULL,
    created_at        TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    expires_at        TIMESTAMPTZ NOT NULL DEFAULT NOW() + INTERVAL '24 hours'
);
CREATE INDEX idx_ikey_expires ON idempotency_keys (expires_at);

-- Order placement (simplified pseudocode)
BEGIN;
  -- Check for existing key
  SELECT order_id, response_body FROM idempotency_keys
  WHERE idempotency_key = $1 FOR UPDATE;

  IF found THEN
    RETURN cached_response;  -- Idempotent replay
  END IF;

  -- Create the order
  INSERT INTO orders (customer_id, restaurant_id, items, total, status)
  VALUES ($2, $3, $4, $5, 'PENDING') RETURNING order_id;

  -- Fire async payment capture via outbox pattern
  INSERT INTO outbox_events (aggregate_id, event_type, payload)
  VALUES (order_id, 'ORDER_PAYMENT_REQUESTED', $payload);

  -- Record idempotency key
  INSERT INTO idempotency_keys VALUES ($1, order_id, 202, $response);
COMMIT;

Order State Machine

Every order transitions through a well-defined state machine. Illegal transitions are rejected at the application layer — state updates are never accepted via direct database writes from any service other than the Order Service.

PENDING[payment_captured]CONFIRMED[restaurant_accepted]PREPARING[courier_assigned]COURIER_ASSIGNED[courier_arrived_restaurant]PICKED_UP[courier_arrived_customer]DELIVERED
PENDING[payment_failed]PAYMENT_FAILED
CONFIRMED / PREPARING[customer/restaurant cancel]CANCELLED[refund_issued]REFUNDED

Payment Capture via Saga

Payment capture is orchestrated via a choreography-based saga using the Transactional Outbox Pattern. The Order Service writes a PAYMENT_REQUESTED event to the outbox table in the same database transaction that creates the order. A Debezium CDC connector streams the event to the payments.commands Kafka topic. The Payment Service consumes the event, calls the payment processor (Stripe/Adyen), and publishes either PAYMENT_SUCCEEDED or PAYMENT_FAILED. The Order Service listens and transitions state accordingly, triggering downstream dispatch only on success.

4. Restaurant Service — Menu Management, Prep Time & Availability

The Restaurant Service handles three distinct responsibilities that have very different read/write characteristics: menu management (write-heavy during onboarding, read-heavy during browse), real-time availability (high-frequency item 86-ing during busy periods), and prep time estimation (critical input to ETA accuracy).

Menu Storage & Serving

Menu schemas vary enormously across restaurant types — pizza has modifier groups (size, crust, toppings), sushi has combo options, coffee has temperature/milk/syrup combinations. A document store (DynamoDB with a restaurant_id partition key) is ideal for this schema flexibility. The canonical menu document is ~5–50 KB per restaurant and is cached aggressively in CDN edge nodes (CloudFront) with a 5-minute TTL. Cache invalidation is triggered by menu edit events published to Kafka.

Real-Time Item Availability

Restaurants mark items as unavailable (sold out) or pause entirely during rush periods. These signals must propagate to customer apps within seconds to prevent orders for unavailable items. Architecture:

Prep Time Estimation

Prep time is one of the two primary inputs to ETA (alongside travel time). The Restaurant Service computes a dynamic prep time estimate using three signals:

A weighted blend (40% declared, 30% historical, 30% queue model) is computed and published as a prep-time signal every 60 seconds to the ETA Service via Kafka. This signal is also used by the Dispatch Engine to decide when to send a courier offer — targeting arrival at the restaurant within 2 minutes of order readiness to minimize wait time.

5. Courier Dispatch Engine — H3 Grid Search, Batching & Acceptance Flow

The Dispatch Engine is the algorithmic heart of the platform. Its job is to solve a continuous, real-time assignment problem: given a set of available couriers and a set of unassigned orders, find the optimal matching that minimizes total delivery time while maximizing courier utilization. At DoorDash scale, this must happen in under 3 seconds for millions of events per hour.

Geospatial Indexing with H3

Uber's H3 hierarchical hexagonal grid is the industry standard for geospatial dispatch. Every latitude/longitude coordinate is mapped to a hex cell at multiple resolutions. For courier dispatch, resolution 9 (average area ~0.1 km²) provides the right granularity for urban areas.

// Courier position stored in Redis using H3 cell index
// Key: h3:{resolution}:{h3_cell_index}  →  SortedSet of courier_ids by score=timestamp

// When a new order is placed at restaurant lat/lng:
String restaurantCell = H3Core.latLngToCell(lat, lng, resolution=9);

// Expanding ring search: k=1 (7 cells) → k=2 (19 cells) → k=3 (37 cells)
for (int ring = 1; ring <= MAX_RING; ring++) {
    List<String> ringCells = H3Core.gridDisk(restaurantCell, ring);
    List<Courier> candidates = redis.sunion(
        ringCells.stream().map(c -> "h3:9:" + c).toArray()
    );
    if (candidates.size() >= MIN_CANDIDATES) break;
}

// Score each candidate courier by: distance + prep_time_remaining + batching_bonus
candidates.sort(Comparator.comparingDouble(c -> scoreCourier(c, order)));

// Send offer to top-scored courier with 30-second acceptance window
dispatchOffer(candidates.get(0), order, offerTtlSeconds=30);

Key advantages of H3 over naive radius search: hex cells tile perfectly (no gaps or overlaps), neighbors are equidistant (no corner bias as with square grids), and the hierarchical structure allows resolution switching for different search radii without re-indexing. Courier position updates in Redis are O(1) set operations; ring lookups are O(k²) but with very small k in practice.

Order Batching & Stacking Algorithm

Order batching (combining multiple orders for one courier trip) dramatically improves unit economics — one courier delivering two orders on one trip earns 60–80% the margin of two separate trips at half the variable cost. The batching algorithm runs on every newly confirmed order and evaluates potential stacks:

Acceptance Flow & Fallback

The dispatch offer is sent to the courier app as a push notification and an in-app overlay. The courier has a configurable acceptance window (default 30 seconds). If declined or timed out:

6. Real-Time Location Service — GPS Stream Ingestion & WebSocket Push

The Location Service handles one of the highest-throughput data streams in the platform. With 500,000 active couriers each sending a GPS ping every 5 seconds, that is 100,000 location events per second — sustained, with 3–5× peaks during dinner rush. This requires a purpose-built streaming pipeline, not a general-purpose REST API.

GPS Ingestion Pipeline

The courier mobile SDK sends location pings over a persistent WebSocket connection (not HTTP polling) to the Location Ingestion Gateway. This gateway is a thin, stateless Netty-based server that validates the payload, stamps a server timestamp, and produces to a partitioned Kafka topic courier.location.raw (partitioned by courier_id for in-order processing).

// Kafka message schema: courier.location.raw
{
  "courier_id":    "cour_8f3a91bc",
  "lat":           40.712776,
  "lng":          -74.005974,
  "accuracy_m":    4.2,
  "speed_kmh":     18.3,
  "heading_deg":   247,
  "battery_pct":   68,
  "client_ts_ms":  1712345678901,
  "server_ts_ms":  1712345678950   // stamped by gateway
}

// Kafka topic config for 100k events/sec throughput
Topic: courier.location.raw
Partitions: 256  // courier_id % 256
Replication: 3
Retention: 24 hours (location data not needed long-term in raw form)
Compression: LZ4  // ~40% size reduction on GPS payloads

Stream Processing & Fan-out

A Kafka Streams (or Flink) application consumes courier.location.raw and performs three actions per event:

WebSocket Connection Management

The WebSocket Gateway is a cluster of stateful servers. Each customer app connects to one gateway node for the duration of an active order. The connection is established after order confirmation and terminated after the "Delivered" state is received. Key design decisions:

7. ETA Prediction — ML Features, Model Architecture & Online Recalculation

ETA accuracy is the single metric most correlated with customer satisfaction and repeat ordering. A system that consistently shows 25 minutes but delivers in 35 minutes destroys trust faster than a system that accurately shows 35 minutes. ETA is fundamentally a machine learning problem because the underlying factors (traffic, restaurant throughput, courier skill) are too complex for rule-based estimation.

Two-Stage ETA Architecture

Total ETA = Prep Time ETA + Travel Time ETA (with courier wait buffer). Each component is modelled separately because they have different feature sets and update cadences:

Component Model Type Key Features Update Cadence
Prep Time Gradient Boosted Trees (XGBoost) Restaurant queue depth, order complexity, hour-of-day, day-of-week, current active orders On order placement; every 2 min thereafter
Travel Time Graph Neural Net + OSRM baseline Current courier position, destination, real-time traffic, courier speed history, route congestion Every 30 s or every 100m movement
Buffer Empirical percentile model Handoff variance, elevator wait, parking, weather Static per zone; updated weekly

Feature Engineering for Travel Time

The travel time model consumes a rich feature vector assembled in real time from multiple sources:

Online Recalculation Pipeline

ETA recalculation is triggered by the Location Service every 30 seconds for each active delivery. The ETA Service maintains a feature store (Redis + offline features in DynamoDB) to assemble the feature vector in <50 ms. Model inference runs on CPU-optimized instances using ONNX runtime — each inference takes <10 ms. The updated ETA is:

8. Dynamic Surge Pricing — Demand/Supply Ratio, Zone Pricing & Dasher Incentives

Surge pricing in food delivery has two economic goals: (1) dampen excess demand during peak periods by increasing customer prices, and (2) attract additional supply (couriers) to underserved zones via incentive bonuses. Importantly, customer price surge and courier incentives are independent levers — you may activate courier incentives without changing customer prices in markets where demand elasticity is low.

Zone-Level Supply/Demand Computation

The Pricing Service partitions every city into pricing zones (H3 resolution 6 hexagons, average area ~36 km²). Every 60 seconds, a Flink job computes for each zone:

// Supply/demand ratio per zone (computed every 60 seconds)
supply_score = (available_couriers * 60) / avg_delivery_time_min;
demand_score = new_orders_last_5_min * 12;  // annualize to hourly rate
sd_ratio = supply_score / demand_score;

// Surge multiplier lookup table (calibrated by market)
IF sd_ratio > 1.5: multiplier = 1.0   // healthy supply; no surge
IF sd_ratio > 1.0: multiplier = 1.1   // mild pressure
IF sd_ratio > 0.8: multiplier = 1.25  // moderate surge
IF sd_ratio > 0.6: multiplier = 1.5   // high surge
IF sd_ratio <= 0.6: multiplier = 2.0  // extreme surge; also trigger incentives

// Courier incentive bonuses (independent of customer price)
IF available_couriers < demand_score * 0.5:
  bonus_per_delivery = base_pay * 0.3  // 30% delivery bonus
  push_notification to offline couriers in adjacent zones

Surge Price Display & Transparency

The surge multiplier is fetched from Redis (TTL 90 seconds) during checkout price computation. The customer app shows a "Busy period — delivery fee increased" banner with the current multiplier when surge is active. Design constraints:

9. Notification & Communication — Push, SMS, Email & Rate Limiting

Order state transitions generate notifications across multiple channels — customer push, restaurant tablet push, courier push, SMS fallback for critical states, and email for receipts. A dedicated Notification Service decouples notification logic from business logic and provides rate limiting, templating, channel fallback, and delivery tracking.

Notification Triggers by State Transition

Event Customer Courier Restaurant
ORDER_CONFIRMED Push + Email (receipt) Tablet push + sound alert
COURIER_ASSIGNED Push (courier info + ETA) Push (dispatch offer)
COURIER_ARRIVING_RESTAURANT Tablet push (2-min warning)
PICKED_UP Push (en route + ETA)
COURIER_NEARBY (500m) Push + SMS fallback
DELIVERED Push + Email (review prompt) Push (earnings summary)

Rate Limiting & Channel Fallback

Push notification delivery is not guaranteed (device offline, DND mode, permission revoked). The Notification Service implements:

10. Capacity Estimation — Peak Orders, GPS Events & Bandwidth

Back-of-the-envelope estimation demonstrates you understand system scale and helps drive architectural decisions. Here is a worked example for a DoorDash-scale platform serving 10 major metros globally.

Order Volume

GPS Event Volume

WebSocket Connections & Bandwidth

11. Scalability & Reliability Patterns

Food delivery is a geo-local problem — orders in New York have zero direct interaction with orders in London. This geographic independence is the primary scaling axis. Every hot path in the system is designed to be partitioned by zone, with zone-level failover and independent scaling budgets.

Zone-Based Partitioning

Every service that processes order or location data is partitioned by a zone_id derived from the restaurant's H3 cell at resolution 4 (large hexagons covering ~2,100 km²). This means:

Circuit Breakers & Graceful Degradation

Every downstream dependency call is wrapped in a circuit breaker (Resilience4j). Defined degradation modes per failure scenario:

Multi-Region Active-Active

The platform deploys across three AWS regions (us-east-1, eu-west-1, ap-southeast-1) in an active-active configuration. Traffic is routed to the nearest region via AWS Route 53 latency routing. Cross-region concerns:

12. System Design Interview Checklist

When asked to design a food delivery system in an interview, structure your answer in this order. Interviewers at DoorDash, Uber Eats, and Amazon have confirmed this progression earns the highest signal:

Food Delivery System Design Checklist

  • Requirements (5 min): Clarify functional scope (customer/courier/restaurant), scale (orders/day, active couriers), SLAs (latency, ETA accuracy), and explicit non-goals (payments processor internals, driver background checks).
  • Capacity estimation (3 min): Orders/sec at peak, GPS events/sec, WebSocket connections, storage growth. Name specific numbers.
  • Core API design (5 min): Define POST /orders with idempotency key, GET /orders/{id}/track, PUT /couriers/location.
  • Data model (5 min): Orders table (PostgreSQL), Menu document (DynamoDB/MongoDB), Courier positions (Redis GEO), Location history (TimescaleDB or Cassandra).
  • Order service (5 min): Idempotent placement, Transactional Outbox for payment, state machine.
  • Dispatch engine (8 min): H3 cell indexing in Redis, expanding ring search, scoring function, batch stacking, acceptance timeout and fallback cascade.
  • Location & WebSocket (5 min): Kafka ingestion pipeline, stream processor for Redis + ETA trigger + customer push, WebSocket Gateway fan-out.
  • ETA (5 min): Two-stage model (prep + travel), feature list, online recalculation trigger, graceful fallback.
  • Surge pricing (3 min): S/D ratio computation, zone-level multipliers, courier incentive bonuses, regulatory caps.
  • Reliability (5 min): Geographic zone partitioning, circuit breakers with named degradation modes, multi-region active-active, RTO/RPO targets.
  • Bottlenecks & trade-offs (3 min): Proactively name the hard problems: dispatch latency under heavy load, ETA accuracy vs compute cost, GPS battery drain vs freshness trade-off.

Common Mistakes to Avoid

Leave a Comment

Related Posts

Md Sanwar Hossain - Software Engineer
Md Sanwar Hossain

Software Engineer · Java · Spring Boot · Microservices · AI/LLM Systems

All Posts
Last updated: April 7, 2026