Complete System Design Guide

Design Food Delivery Platform

Build a platform like Uber Eats, DoorDash, Swiggy, or Zomato from scratch

Master restaurant discovery, real-time tracking, delivery partner matching, ETA prediction, and handling millions of orders during peak hours

All System Design

What You Will Learn

Problem Statement & Requirements
Scale & Traffic Estimation
High-Level Architecture
Restaurant Discovery & Search
Order Management Pipeline
Delivery Partner Matching
Real-time Location Tracking
ETA Prediction System
Handling Peak Hours (Lunch/Dinner)
Database Design & Sharding
Caching Strategies
Notifications & Updates

1Understanding the Problem

A Food Delivery Platform is a three-sided marketplace connecting customers, restaurants, and delivery partners. Unlike traditional e-commerce, it involves perishable goods, time-sensitive operations, and complex real-time coordination between multiple parties.

The Three Actors

Customers

Browse restaurants, place orders, track delivery, rate experience

Restaurants

Manage menu, accept orders, prepare food, update status

Delivery Partners

Accept deliveries, pick up food, navigate to customer, complete delivery

Functional Requirements

  • Browse nearby restaurants based on location
  • Search restaurants by cuisine, dish, or name
  • View menus with prices and availability
  • Place orders and make secure payments
  • Real-time order tracking with live map
  • ETA updates throughout the delivery process
  • Rate and review restaurants and drivers

Non-Functional Requirements

  • Latency: Restaurant list in under 2 seconds
  • Availability: 99.9% uptime during peak hours
  • Scale: Millions of orders per day
  • Real-time: Location updates every 3-5 seconds
  • Consistency: No double-assignment of drivers
  • ETA Accuracy: Within 5 minutes of actual

2Scale & Traffic Estimation

Let's estimate the scale for a platform operating in a major metro city.

Scale Assumptions

  • Restaurants: 50,000 active restaurants per city
  • Delivery Partners: 100,000 active drivers per city
  • Daily Orders: 2 million orders per city
  • Peak Hours: 12-2 PM (lunch) and 7-10 PM (dinner) = 60% of orders
  • Average Order Value: $20-30
  • Delivery Radius: 5-10 km from restaurant

Traffic Calculations:

// Average Orders Per Second

2M orders / 86,400 sec ≈ 23 orders/sec

// Peak Hour Orders (60% in 6 hours)

1.2M orders / 6 hours / 3600 sec ≈ 55 orders/sec

// Location Updates (100K drivers × every 5 sec)

100,000 / 5 = 20,000 location updates/sec

// Restaurant Search Queries

10x order rate ≈ 550 searches/sec during peak

Key Insight: The system has two critical real-time components: location tracking (20K updates/sec) and order state changes. Both require low-latency infrastructure and careful event processing.

3High-Level Architecture

System Architecture


┌─────────────────────────────────────────────────────────────────────────────┐
│                              CLIENT APPS                                     │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐                       │
│  │ Customer App │  │Restaurant App│  │  Driver App  │                       │
│  │   (iOS/Web)  │  │  (Tablet)    │  │   (Mobile)   │                       │
│  └──────────────┘  └──────────────┘  └──────────────┘                       │
└─────────────────────────────────────────────────────────────────────────────┘
                                        │
                           ┌────────────┴────────────┐
                           │      API Gateway        │
                           │   (Rate Limit, Auth)    │
                           └────────────┬────────────┘
                                        │
┌─────────────────────────────────────────────────────────────────────────────┐
│                            CORE SERVICES                                     │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│  ┌────────────┐  ┌────────────┐  ┌────────────┐  ┌────────────┐             │
│  │ Restaurant │  │   Order    │  │  Delivery  │  │   Search   │             │
│  │  Service   │  │  Service   │  │  Service   │  │  Service   │             │
│  └────────────┘  └────────────┘  └────────────┘  └────────────┘             │
│                                                                              │
│  ┌────────────┐  ┌────────────┐  ┌────────────┐  ┌────────────┐             │
│  │  Location  │  │   ETA      │  │  Payment   │  │   User     │             │
│  │  Service   │  │  Service   │  │  Service   │  │  Service   │             │
│  └────────────┘  └────────────┘  └────────────┘  └────────────┘             │
│                                                                              │
│  ┌────────────┐  ┌────────────┐  ┌────────────┐  ┌────────────┐             │
│  │Notification│  │  Rating    │  │  Pricing   │  │  Promo     │             │
│  │  Service   │  │  Service   │  │  Service   │  │  Service   │             │
│  └────────────┘  └────────────┘  └────────────┘  └────────────┘             │
└─────────────────────────────────────────────────────────────────────────────┘
                                        │
┌─────────────────────────────────────────────────────────────────────────────┐
│                           DATA & MESSAGING                                   │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐     │
│  │  PostgreSQL  │  │    Redis     │  │Elasticsearch │  │    Kafka     │     │
│  │   (Orders)   │  │  (Location)  │  │  (Search)    │  │   (Events)   │     │
│  └──────────────┘  └──────────────┘  └──────────────┘  └──────────────┘     │
│                                                                              │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐                       │
│  │   MongoDB    │  │  TimeSeries  │  │   S3/CDN     │                       │
│  │  (Menus)     │  │    (ETA)     │  │  (Images)    │                       │
│  └──────────────┘  └──────────────┘  └──────────────┘                       │
└─────────────────────────────────────────────────────────────────────────────┘

Key Services Explained

  • Restaurant Service: Menu, hours, availability
  • Order Service: Cart, checkout, order state
  • Delivery Service: Driver matching, assignment
  • Location Service: Real-time GPS tracking
  • ETA Service: Time predictions using ML
  • Search Service: Restaurant discovery

Why These Technology Choices?

  • Redis: Sub-millisecond location lookups
  • Elasticsearch: Fast geo + text search
  • Kafka: Async event processing at scale
  • MongoDB: Flexible menu schemas
  • TimeSeries DB: ETA model training data
  • PostgreSQL: ACID for orders & payments

4Restaurant Discovery & Search

Users need to quickly find restaurants based on location, cuisine, ratings, and availability.

Geospatial Indexing with Geohash

Geohash converts latitude/longitude into a string. Nearby locations share the same prefix, enabling efficient range queries.

Location: 40.7128° N, 74.0060° W (New York)
Geohash:  dr5ru7 (precision 6 = ~1.2km × 0.6km cell)

Nearby Search: Find all restaurants where geohash LIKE 'dr5ru%'
This is much faster than calculating distance for every restaurant!

-- Elasticsearch geo_distance query
{
  "query": {
    "bool": {
      "must": { "match": { "cuisine": "italian" } },
      "filter": {
        "geo_distance": {
          "distance": "5km",
          "location": { "lat": 40.7128, "lon": -74.0060 }
        }
      }
    }
  },
  "sort": [
    { "_geo_distance": { "location": [40.7128, -74.0060], "order": "asc" } },
    { "rating": "desc" }
  ]
}

Restaurant Ranking Factors

Distance
30%
Rating & Reviews
25%
Estimated Delivery Time
20%
Popularity (recent orders)
15%
Promoted/Sponsored
10%

Pro Tip: Pre-compute restaurant rankings for each geohash cell during off-peak hours. Serve from cache during peak. Update cache every 5 minutes.

5Order Management Pipeline

Order State Machine


CART
  │
  ▼ [Checkout initiated]
PENDING_PAYMENT
  │
  ├──▶ [Payment failed] ──▶ PAYMENT_FAILED
  │
  ▼ [Payment success]
PLACED
  │
  ├──▶ [Restaurant rejects] ──▶ REJECTED (refund triggered)
  │
  ▼ [Restaurant accepts]
CONFIRMED
  │
  ▼ [Food being prepared]
PREPARING
  │
  ▼ [Food ready, driver assigned]
READY_FOR_PICKUP
  │
  ▼ [Driver picks up]
PICKED_UP
  │
  ▼ [Driver en route]
OUT_FOR_DELIVERY
  │
  ├──▶ [Customer unavailable] ──▶ DELIVERY_FAILED
  │
  ▼ [Delivered successfully]
DELIVERED
  │
  ▼ [After 24 hours]
COMPLETED (ratings enabled)

Order Processing Flow

1

Customer Places Order

Validate cart items, calculate pricing with taxes & delivery fee, initiate payment

2

Payment Processing

Async payment via Stripe/Razorpay. Order stays in PENDING_PAYMENT until confirmed.

3

Restaurant Notification

Push notification to restaurant app. They have 60 seconds to accept or auto-reject.

4

Driver Assignment (Parallel)

Start looking for drivers immediately. Don't wait for restaurant to accept.

5

Preparation Tracking

Restaurant updates prep status. Driver arrives just as food is ready.

Kafka Events for Order Flow

// Kafka Topics
order.placed         → Triggers: payment processing, restaurant notification
order.confirmed      → Triggers: driver assignment, ETA calculation
order.preparing      → Triggers: customer notification, driver dispatch timing
order.ready          → Triggers: driver notification to head to restaurant
order.picked-up      → Triggers: customer notification, live tracking enabled
order.delivered      → Triggers: payment settlement, rating prompt

// Consumer Groups
- payment-processor (order.placed)
- restaurant-notifier (order.placed)
- delivery-matcher (order.confirmed)
- eta-calculator (order.confirmed, order.picked-up)
- notification-sender (all events)
- analytics-collector (all events)

6Delivery Partner Matching

Finding the optimal driver is critical for delivery time and driver satisfaction.

Matching Algorithm

function findOptimalDriver(order, restaurant) {
  // Step 1: Get nearby available drivers
  const nearbyDrivers = await redis.georadius(
    'driver_locations',
    restaurant.longitude,
    restaurant.latitude,
    3000, // 3km radius
    'WITHDIST'
  );

  // Step 2: Filter by status
  const availableDrivers = nearbyDrivers.filter(d =>
    d.status === 'ONLINE' &&
    d.currentOrders < d.maxConcurrentOrders &&
    !d.isOnBreak
  );

  // Step 3: Score each driver
  const scoredDrivers = availableDrivers.map(driver => ({
    driver,
    score: calculateDriverScore(driver, order, restaurant)
  }));

  // Step 4: Sort and pick top driver
  scoredDrivers.sort((a, b) => b.score - a.score);

  // Step 5: Send offer to top driver
  const topDriver = scoredDrivers[0];
  await sendDeliveryOffer(topDriver.driver, order, timeout: 30);

  // If rejected/timeout, try next driver
}

function calculateDriverScore(driver, order, restaurant) {
  let score = 0;

  // Distance to restaurant (35%)
  const distToRestaurant = driver.distanceMeters;
  score += (1 - distToRestaurant / 3000) * 35;

  // Driver rating (20%)
  score += (driver.rating / 5) * 20;

  // Acceptance rate (15%)
  score += driver.acceptanceRate * 15;

  // Time since last delivery (15%) - avoid idle drivers
  const idleMinutes = (Date.now() - driver.lastDeliveryAt) / 60000;
  score += Math.min(idleMinutes / 30, 1) * 15;

  // Vehicle type match (15%)
  if (order.requiresLargeVehicle && driver.vehicleType === 'CAR') {
    score += 15;
  } else if (!order.requiresLargeVehicle) {
    score += 15; // Bike is fine
  }

  return score;
}

Order Batching

One driver can pick up from multiple restaurants if they're close and going in the same direction.

Rules:
  • • Max 2 orders per batch
  • • Restaurants within 500m of each other
  • • Delivery addresses within 1km of each other
  • • Total additional time < 10 minutes

Driver Reassignment

If assigned driver becomes unavailable, quickly reassign without impacting delivery time.

Triggers:
  • • Driver cancels assignment
  • • Driver goes offline
  • • Driver stuck in traffic (ETA > threshold)
  • • Vehicle breakdown reported

7Real-time Location Tracking

Handling 20,000+ location updates per second requires specialized infrastructure.

Location Update Pipeline


Driver App                 Location Service              Redis Geo           Customer App
    │                            │                           │                    │
    │  POST /location            │                           │                    │
    │  {lat, lng, timestamp}     │                           │                    │
    │ ──────────────────────────▶│                           │                    │
    │                            │                           │                    │
    │                            │  GEOADD driver_locations  │                    │
    │                            │  driver_123 lng lat       │                    │
    │                            │ ─────────────────────────▶│                    │
    │                            │                           │                    │
    │                            │  PUBLISH driver:123       │                    │
    │                            │  {lat, lng, heading}      │                    │
    │                            │ ─────────────────────────▶│                    │
    │                            │                           │                    │
    │                            │                           │  WebSocket push    │
    │                            │                           │ ──────────────────▶│
    │                            │                           │                    │
    │                            │                           │  Update map marker │
    │                            │                           │                    │


// Redis commands for location
GEOADD driver_locations -73.935242 40.730610 driver_123
GEORADIUS driver_locations -73.935 40.730 3 km WITHDIST WITHCOORD

// Pub/Sub for live tracking
PUBLISH order:456:location '{"lat":40.730,"lng":-73.935,"eta":5}'

Location Update Frequency

  • Active delivery: Every 3-5 seconds
  • Idle/waiting: Every 30 seconds
  • Offline: No updates (battery saving)
  • Background: Significant location change only

Client-side Smoothing

GPS can be jumpy. Smooth the marker movement on the customer app.

// Animate marker between updates
animateMarker(
  currentPos,
  newPos,
  duration: 3000ms,
  easing: 'linear'
);

8ETA Prediction System

Accurate ETAs build customer trust. An ML model predicts delivery time based on multiple factors.

ETA Components

Total ETA = Restaurant Accept Time
           + Food Preparation Time
           + Driver Arrival at Restaurant
           + Pickup Time
           + Travel Time to Customer
           + Handoff Time

// Each component predicted separately
{
  "restaurant_accept": "2 min (avg for this restaurant)",
  "preparation": "18 min (based on order items + current load)",
  "driver_to_restaurant": "5 min (real-time traffic)",
  "pickup": "3 min (avg for this restaurant)",
  "delivery_travel": "8 min (Google Maps API + ML adjustment)",
  "handoff": "2 min (building type factor)",
  "total_eta": "38 min",
  "confidence": 0.85
}

ML Model Features

Time-based

Hour of day, day of week, holidays, weather

Restaurant-based

Historical prep time, current queue, cuisine type

Location-based

Traffic patterns, distance, building access difficulty

Key Insight: Update ETA dynamically as order progresses. A stale ETA (still showing 30 min when food is ready) frustrates customers. Push new ETA on every state change.

9Handling Peak Hours

The Challenge

During lunch (12-2 PM) and dinner (7-10 PM), order volume spikes 3-5x. Additionally, cricket match finals or New Year's Eve can cause 10x spikes. The system must handle this gracefully.

Strategy 1: Surge Pricing

When demand exceeds supply, increase delivery fee to balance the market.

demandSupplyRatio = pendingOrders / availableDrivers

if (ratio > 1.5) surgeFee = baseFee * 1.3  // 30% surge
if (ratio > 2.0) surgeFee = baseFee * 1.5  // 50% surge
if (ratio > 3.0) surgeFee = baseFee * 2.0  // 100% surge

// Show surge warning before checkout
"Delivery fee is higher than usual due to high demand"

Strategy 2: Increase ETA Transparently

Don't promise 30 min if you can't deliver. Show realistic ETAs.

// During peak hours
baseETA = calculateBaseETA(order);
peakMultiplier = getPeakMultiplier(currentHour, dayOfWeek);
adjustedETA = baseETA * peakMultiplier;

// Show on UI: "Delivery in 45-55 min (Busy time)"
// Customer can decide if they want to wait or cook at home

Strategy 3: Driver Incentives

Notify offline drivers about surge zones to increase supply.

  • • "High demand in Downtown! Earn 2x per delivery for next 2 hours"
  • • "Complete 5 orders by 9 PM, get $20 bonus"
  • • Heat maps showing high-demand areas

Strategy 4: Auto-scaling Infrastructure

# Pre-scheduled scaling for known peak hours
0 11 * * * kubectl scale deployment order-service --replicas=20
0 14 * * * kubectl scale deployment order-service --replicas=10
0 18 * * * kubectl scale deployment order-service --replicas=25
0 22 * * * kubectl scale deployment order-service --replicas=10

# Event-driven scaling for sudden spikes
HPA with custom metrics:
- orders_per_second > 50 → scale up
- orders_per_second < 20 → scale down (with delay)

10Database Design

Core Tables

-- Restaurants
CREATE TABLE restaurants (
    restaurant_id   UUID PRIMARY KEY,
    name            VARCHAR(200),
    location        GEOGRAPHY(Point, 4326),
    city_id         VARCHAR(10),
    cuisine_types   TEXT[],
    rating          DECIMAL(2,1),
    is_open         BOOLEAN,
    avg_prep_time   INT,  -- minutes
    created_at      TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_restaurant_geo ON restaurants USING GIST(location);

-- Menu Items
CREATE TABLE menu_items (
    item_id         UUID PRIMARY KEY,
    restaurant_id   UUID REFERENCES restaurants(restaurant_id),
    name            VARCHAR(200),
    description     TEXT,
    price           DECIMAL(10,2),
    category        VARCHAR(50),
    is_available    BOOLEAN DEFAULT true,
    prep_time_mins  INT
);

-- Orders (Sharded by city_id)
CREATE TABLE orders (
    order_id        UUID PRIMARY KEY,
    customer_id     UUID,
    restaurant_id   UUID,
    driver_id       UUID,
    city_id         VARCHAR(10),
    status          VARCHAR(30),
    total_amount    DECIMAL(10,2),
    delivery_fee    DECIMAL(10,2),
    delivery_address JSONB,
    delivery_location GEOGRAPHY(Point, 4326),
    placed_at       TIMESTAMP,
    delivered_at    TIMESTAMP,
    eta_minutes     INT
);

-- Delivery Partners
CREATE TABLE delivery_partners (
    driver_id       UUID PRIMARY KEY,
    name            VARCHAR(100),
    phone           VARCHAR(20),
    vehicle_type    VARCHAR(20),
    rating          DECIMAL(2,1),
    status          VARCHAR(20), -- ONLINE, OFFLINE, ON_DELIVERY
    current_location GEOGRAPHY(Point, 4326),
    city_id         VARCHAR(10),
    last_active     TIMESTAMP
);

Sharding Strategy

  • Orders: Shard by city_id
  • Restaurants: Shard by city_id
  • Drivers: Shard by city_id
  • Users: Shard by user_id hash
  • Menus: Co-locate with restaurants

Read Replicas

  • Search queries: Read from replicas
  • Order writes: Primary only
  • Analytics: Dedicated replica
  • Replication lag: <100ms acceptable

11Notifications & Real-time Updates

Notification Triggers

Customer Notifications

  • • Order confirmed by restaurant
  • • Driver assigned
  • • Driver picked up order
  • • Driver arriving (5 min away)
  • • Order delivered
  • • Rate your experience

Driver Notifications

  • • New delivery offer
  • • Order ready for pickup
  • • Customer address update
  • • Bonus/incentive alerts
  • • Surge zone notifications

Multi-channel Delivery

// Notification Service
async function sendNotification(userId, event, data) {
  const user = await getUser(userId);
  const message = formatMessage(event, data);

  // 1. In-app (always)
  await pushToWebSocket(userId, message);

  // 2. Push notification (if app in background)
  if (!user.isAppActive) {
    await sendPushNotification(user.fcmToken, message);
  }

  // 3. SMS (for critical events only)
  if (event.isCritical && user.smsEnabled) {
    await sendSMS(user.phone, message.shortText);
  }

  // 4. Email (for receipts, not time-sensitive)
  if (event === 'ORDER_DELIVERED') {
    await queueEmail(user.email, 'receipt', data);
  }
}

Key Design Principles

  • Three-sided Marketplace: Optimize for customers, restaurants, AND drivers. Balance all three.
  • Location is Everything: Use Redis Geo + Elasticsearch for fast nearby queries. Update driver locations in real-time.
  • Parallel Operations: Start driver matching while restaurant is confirming. Every minute saved matters.
  • Dynamic ETA: Update ETA on every state change. Use ML to predict prep time, traffic, and delivery time.
  • Peak Hour Strategy: Surge pricing, realistic ETAs, driver incentives, and auto-scaling infrastructure.
  • City-based Sharding: Orders and restaurants are naturally city-scoped. Shard for isolation and scale.

Ready to Design Food Delivery?

Practice with an AI interviewer and get instant feedback on your system design skills.

Related System Design Questions: