System Design Interview: Food Delivery App (DoorDash / UberEats)

Food delivery is a three-sided marketplace connecting customers, restaurants, and delivery drivers — more complex than ride-sharing because orders involve a waiting period at the restaurant, menu management, and payment split across three parties. DoorDash, Uber Eats, and Grubhub face unique challenges around order batching, restaurant integration, and dynamic delivery time estimation.

Three-Sided Marketplace

Unlike ride-sharing (two parties), food delivery coordinates three independent parties with different needs:

Party Needs Key Data
Customer Browse menus, place orders, track delivery, rate experience Location, payment method, dietary preferences, order history
Restaurant Receive and manage orders, update menu availability, set prep times Menu items, prep time per item, operating hours, busy status
Dasher (Driver) Accept orders, navigate pickup and delivery, maximize earnings Current location, vehicle type, acceptance rate, active deliveries

Functional Requirements

  • Customer: browse restaurant menus, place order, pay, track delivery in real-time
  • Restaurant: receive order notifications, manage order flow (received → preparing → ready), update menu items
  • Driver: receive delivery assignment, navigate to restaurant and customer, mark delivery complete
  • Order management: create, track, cancel, refund
  • Real-time ETA for both restaurant pickup and customer delivery

High-Level Architecture

             [Customer App]    [Restaurant App]    [Driver App]
                    ↓                ↓                  ↓
            [API Gateway + Auth]
            /         |              |              
  [Order Service] [Menu Service] [Restaurant    [Dispatch
                                   Service]      Service]
        |              |              |              |
  [Order DB]     [Menu Cache]  [Restaurant DB]  [Driver
  (Postgres)      (Redis)       (Postgres)    Location]
        |                                      (Redis Geo)
  [Payment Service]
        |
  [Stripe / Payment Gateway]
        ↓
  [Notification Service]  →  Customer, Restaurant, Driver push/SMS

Order State Machine

Customer places order
        ↓
    CREATED
        ↓ [Payment authorized]
    PAID
        ↓ [Sent to restaurant]
    SENT_TO_RESTAURANT
        ↓ [Restaurant accepts]
    CONFIRMED
        ↓ [Restaurant starts cooking]
    PREPARING
        ↓ [Driver assigned]
    DRIVER_ASSIGNED
        ↓ [Driver arrives at restaurant]
    DRIVER_AT_RESTAURANT
        ↓ [Restaurant marks order ready, driver picks up]
    PICKED_UP
        ↓ [Driver arrives at customer]
    DELIVERED
        ↓ [Customer rates, payment settled]
    COMPLETED

Side transitions:
    CREATED → CANCELLED (customer cancels before payment)
    PAID → REFUNDED (restaurant rejects or is closed)
    PREPARING → CANCELLED_LATE (restaurant cannot fulfill — compensation to customer)
// Order state transitions in code
class OrderService:
    def confirm_order(self, order_id: str, restaurant_id: str):
        order = db.get_order(order_id)
        assert order.status == OrderStatus.SENT_TO_RESTAURANT
        assert order.restaurant_id == restaurant_id

        order.status = OrderStatus.CONFIRMED
        order.confirmed_at = datetime.now()

        # Estimate prep time and start driver search
        prep_time = estimate_prep_time(order)
        dispatch_service.find_driver(order, available_in=prep_time)

        db.save(order)
        notifications.notify_customer(order, "Your order is confirmed!")

Menu Management

-- Menu schema
CREATE TABLE restaurants (id, name, address, lat, lon, operating_hours, avg_rating);
CREATE TABLE menu_categories (id, restaurant_id, name, display_order);
CREATE TABLE menu_items (
    id          BIGINT PRIMARY KEY,
    category_id BIGINT REFERENCES menu_categories,
    name        VARCHAR(255) NOT NULL,
    description TEXT,
    price_cents INT NOT NULL,
    is_available BOOLEAN DEFAULT TRUE,
    prep_time_minutes INT
);
CREATE TABLE item_options (id, item_id, option_name, choices JSON, required BOOLEAN);

-- Redis cache for active restaurant menus (TTL 5 minutes)
-- Key: menu:{restaurant_id}
-- Value: full serialized menu JSON
-- Invalidated when restaurant updates any item

-- Hot path: 95% of menu views served from Redis
-- Cold path: cache miss → PostgreSQL → rebuild cache

Driver Dispatch and Order Batching

Basic Dispatch

Similar to ride-sharing: find available drivers near the restaurant using Redis GEORADIUS, estimate pickup time, offer to best candidate with timeout.

Order Batching (Stacked Orders)

DoorDash and Uber Eats allow drivers to pick up orders from multiple restaurants in one trip. The algorithm evaluates whether adding a new order to an in-progress delivery increases total earnings relative to total added time:

def should_batch(driver, new_order, active_delivery):
    # Driver is heading to/from restaurant A
    # Evaluate adding restaurant B and customer B to the route

    # Extra distance for batched route vs direct route
    current_route_time = route_time(driver.pos, active_delivery.restaurant,
                                    active_delivery.customer)
    batched_route_time = optimized_route_time([
        driver.pos,
        active_delivery.restaurant,
        new_order.restaurant,
        active_delivery.customer,
        new_order.customer,
    ])

    added_time = batched_route_time - current_route_time

    # Batch if: added time  600:   # more than 10 minutes added
        return False

    new_eta = current_time() + batched_route_time
    if new_eta > new_order.promised_delivery_time:
        return False

    return True

Real-Time ETA Computation

Delivery ETA = restaurant prep time + driver travel to restaurant + driver travel to customer.

def estimate_delivery_eta(order, driver_assigned=None) -> datetime:
    restaurant = get_restaurant(order.restaurant_id)

    # Prep time: ML model trained on restaurant + item + current busy-ness
    prep_time = ml_prep_estimator.predict(
        restaurant_id=order.restaurant_id,
        items=order.items,
        current_order_queue_length=restaurant.active_order_count,
        time_of_day=datetime.now().hour,
    )

    if driver_assigned:
        # Time for driver to reach restaurant
        driver_to_restaurant = route_eta(driver_assigned.position,
                                         restaurant.location)
        # Total wait = max(prep done time, driver arrival time)
        pickup_time = max(
            datetime.now() + timedelta(seconds=prep_time),
            datetime.now() + timedelta(seconds=driver_to_restaurant),
        )
    else:
        # No driver yet — estimate when one will be available
        pickup_time = datetime.now() + timedelta(seconds=prep_time + 300)

    # Restaurant to customer
    restaurant_to_customer = route_eta(restaurant.location, order.delivery_address)

    return pickup_time + timedelta(seconds=restaurant_to_customer)

Payment and Settlement

Food delivery payments are complex — money flows between four parties:

  1. Customer pays: full amount (food + delivery fee + tip + service fee) → DoorDash
  2. DoorDash pays restaurant: food subtotal minus platform commission (15-30%)
  3. DoorDash pays driver: delivery fee + tip + any promotions (weekly settlement)
  4. DoorDash keeps: service fee + platform commission

Use Stripe Connect or similar for marketplace payments with automatic split and payout. All settlements use an event-driven ledger for auditability.

Interview Discussion Points

  • How do you handle restaurant order rejection? Auto-refund with coupon, offer customer alternative restaurants, penalize restaurant for frequent rejections
  • How do you estimate restaurant prep time accurately? ML model: historical prep time per restaurant, item complexity, queue length at order time, day/time features. Real-time updates from restaurant app as they start and complete orders
  • How do you prevent driver fraud (marking delivered without delivering)? Geofencing: delivery must be confirmed within 100m of customer address; photo proof upload; customer feedback loop
  • How do you handle surge demand during lunch rush? Pre-scale driver supply with guaranteed earnings promotions in advance; adjust restaurant estimated prep times based on queue length; show longer ETA quotes to manage customer expectations

Frequently Asked Questions

How does a food delivery app coordinate between three parties?

A food delivery platform is a three-sided marketplace: customer, restaurant, and driver each have separate apps and distinct needs. The coordination flow: (1) Customer places an order; payment is authorized immediately. (2) Order is sent to the restaurant app and enters SENT_TO_RESTAURANT state. (3) Restaurant confirms and begins preparation; this triggers the dispatch service to find an available driver estimated to arrive when food is ready. (4) Driver accepts, picks up food, and navigates to customer. (5) Delivery is confirmed; payment is settled and split: restaurant receives food subtotal minus platform commission, driver receives delivery fee plus tip, platform keeps the service fee. Each state transition fires notifications to all relevant parties.

How does a food delivery system estimate delivery time?

Delivery ETA has three components: restaurant prep time, driver travel to restaurant, and driver travel from restaurant to customer. Restaurant prep time is estimated by an ML model trained on historical data, using features like restaurant ID, ordered items, current queue length, and time of day. Driver travel times use real-time road network routing (Google Maps API or an internal routing engine) with live traffic data. The total ETA is: max(prep done time, driver arrival time) + restaurant-to-customer travel time. The ETA is continuously updated as the order progresses — when a driver accepts, the ETA becomes more accurate; when food is ready and driver picks up, the final delivery ETA becomes precise.

How does order batching (stacked orders) work in DoorDash?

Order batching lets a single driver pick up from multiple restaurants in one trip, increasing driver earnings and platform efficiency. The algorithm evaluates whether adding a new pickup to an in-progress delivery is worth the added time: it compares the total route time of the combined delivery (both pickups and both drop-offs in optimized order) against the original single delivery route. If the added time is under a threshold (typically 8-10 minutes) AND both customers still receive delivery within their promised windows, the system offers the second order to the driver. The batching decision is made automatically before the driver is offered the additional order, so drivers see a combined offer with both pickup addresses and estimated earnings.

{
“@context”: “https://schema.org”,
“@type”: “FAQPage”,
“mainEntity”: [
{
“@type”: “Question”,
“name”: “How does a food delivery app coordinate between three parties?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “A food delivery platform is a three-sided marketplace: customer, restaurant, and driver each have separate apps and distinct needs. The coordination flow: (1) Customer places an order; payment is authorized immediately. (2) Order is sent to the restaurant app and enters SENT_TO_RESTAURANT state. (3) Restaurant confirms and begins preparation; this triggers the dispatch service to find an available driver estimated to arrive when food is ready. (4) Driver accepts, picks up food, and navigates to customer. (5) Delivery is confirmed; payment is settled and split: restaurant receives food subtotal minus platform commission, driver receives delivery fee plus tip, platform keeps the service fee. Each state transition fires notifications to all relevant parties.”
}
},
{
“@type”: “Question”,
“name”: “How does a food delivery system estimate delivery time?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Delivery ETA has three components: restaurant prep time, driver travel to restaurant, and driver travel from restaurant to customer. Restaurant prep time is estimated by an ML model trained on historical data, using features like restaurant ID, ordered items, current queue length, and time of day. Driver travel times use real-time road network routing (Google Maps API or an internal routing engine) with live traffic data. The total ETA is: max(prep done time, driver arrival time) + restaurant-to-customer travel time. The ETA is continuously updated as the order progresses — when a driver accepts, the ETA becomes more accurate; when food is ready and driver picks up, the final delivery ETA becomes precise.”
}
},
{
“@type”: “Question”,
“name”: “How does order batching (stacked orders) work in DoorDash?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Order batching lets a single driver pick up from multiple restaurants in one trip, increasing driver earnings and platform efficiency. The algorithm evaluates whether adding a new pickup to an in-progress delivery is worth the added time: it compares the total route time of the combined delivery (both pickups and both drop-offs in optimized order) against the original single delivery route. If the added time is under a threshold (typically 8-10 minutes) AND both customers still receive delivery within their promised windows, the system offers the second order to the driver. The batching decision is made automatically before the driver is offered the additional order, so drivers see a combined offer with both pickup addresses and estimated earnings.”
}
}
]
}

Companies That Ask This Question

Scroll to Top