Low-Level Design: Food Delivery Order System (DoorDash/Uber Eats) — State Machine, Driver Assignment

Requirements

Functional: customers browse restaurants and place orders, restaurants receive and confirm orders, the platform assigns delivery drivers, drivers pick up and deliver orders, real-time status tracking (order → confirmed → preparing → ready → picked_up → delivered), cancellation handling.

Non-functional: order placement is idempotent (no duplicate orders), status transitions are atomic, driver assignment is fair (nearest available driver), system handles peak-time burst (dinner rush).

Core Entities

from enum import Enum
from dataclasses import dataclass, field
from typing import Optional, List
from datetime import datetime

class OrderStatus(Enum):
    PENDING       = "PENDING"        # placed, awaiting restaurant confirmation
    CONFIRMED     = "CONFIRMED"      # restaurant accepted
    PREPARING     = "PREPARING"      # kitchen started
    READY         = "READY"          # ready for pickup
    DRIVER_ASSIGNED = "DRIVER_ASSIGNED"
    PICKED_UP     = "PICKED_UP"      # driver has the food
    DELIVERED     = "DELIVERED"
    CANCELLED     = "CANCELLED"

@dataclass
class OrderItem:
    item_id: str
    name: str
    quantity: int
    unit_price_cents: int

@dataclass
class Order:
    order_id: str
    customer_id: str
    restaurant_id: str
    driver_id: Optional[str]
    items: List[OrderItem]
    status: OrderStatus
    subtotal_cents: int
    delivery_fee_cents: int
    tip_cents: int
    total_cents: int
    delivery_address: str
    special_instructions: str
    placed_at: datetime
    estimated_delivery: Optional[datetime]
    delivered_at: Optional[datetime]
    cancellation_reason: Optional[str] = None

@dataclass
class Driver:
    driver_id: str
    name: str
    is_available: bool
    latitude: float
    longitude: float
    current_order_id: Optional[str]
    rating: float
    total_deliveries: int

@dataclass
class Restaurant:
    restaurant_id: str
    name: str
    is_open: bool
    average_prep_minutes: int
    latitude: float
    longitude: float

Order State Machine

VALID_TRANSITIONS = {
    OrderStatus.PENDING:          [OrderStatus.CONFIRMED, OrderStatus.CANCELLED],
    OrderStatus.CONFIRMED:        [OrderStatus.PREPARING, OrderStatus.CANCELLED],
    OrderStatus.PREPARING:        [OrderStatus.READY, OrderStatus.CANCELLED],
    OrderStatus.READY:            [OrderStatus.DRIVER_ASSIGNED],
    OrderStatus.DRIVER_ASSIGNED:  [OrderStatus.PICKED_UP, OrderStatus.READY],  # driver unassigned -> back to READY
    OrderStatus.PICKED_UP:        [OrderStatus.DELIVERED],
    OrderStatus.DELIVERED:        [],
    OrderStatus.CANCELLED:        [],
}

def transition(order: Order, new_status: OrderStatus, reason: str = '') -> None:
    if new_status not in VALID_TRANSITIONS[order.status]:
        raise ValueError(f"Cannot transition {order.status} -> {new_status}")
    order.status = new_status
    if reason:
        order.cancellation_reason = reason
    db.save(order)
    emit_event('order_status_changed', order)

Order Service

class OrderService:
    def place_order(self, customer_id: str, restaurant_id: str,
                    items: List[dict], delivery_address: str,
                    idempotency_key: str) -> Order:
        # Idempotency: return existing order if key already used
        existing = db.get_order_by_idempotency_key(idempotency_key)
        if existing:
            return existing
        restaurant = db.get_restaurant(restaurant_id)
        if not restaurant.is_open:
            raise ValueError("Restaurant is closed")
        order_items = self._validate_items(restaurant_id, items)
        subtotal = sum(i.unit_price_cents * i.quantity for i in order_items)
        delivery_fee = self._calculate_delivery_fee(restaurant, delivery_address)
        order = Order(
            order_id=generate_id(),
            customer_id=customer_id,
            restaurant_id=restaurant_id,
            driver_id=None,
            items=order_items,
            status=OrderStatus.PENDING,
            subtotal_cents=subtotal,
            delivery_fee_cents=delivery_fee,
            tip_cents=0,
            total_cents=subtotal + delivery_fee,
            delivery_address=delivery_address,
            special_instructions='',
            placed_at=datetime.utcnow(),
            estimated_delivery=self._estimate_delivery(restaurant),
        )
        db.save_order(order, idempotency_key=idempotency_key)
        notify_restaurant(order)
        return order

    def cancel_order(self, order_id: str, reason: str, requester: str) -> Order:
        order = db.get_order(order_id)
        # Business rule: cannot cancel after driver picked up
        if order.status in [OrderStatus.PICKED_UP, OrderStatus.DELIVERED]:
            raise ValueError("Cannot cancel after pickup")
        transition(order, OrderStatus.CANCELLED, reason)
        if order.driver_id:
            self._unassign_driver(order.driver_id)
        self._refund(order, requester)
        return order

Driver Assignment

import math

def haversine_distance(lat1, lon1, lat2, lon2) -> float:
    """Approximate distance in km between two lat/lon points."""
    R = 6371
    dlat = math.radians(lat2 - lat1)
    dlon = math.radians(lon2 - lon1)
    a = math.sin(dlat/2)**2 + math.cos(math.radians(lat1)) * math.cos(math.radians(lat2)) * math.sin(dlon/2)**2
    return R * 2 * math.asin(math.sqrt(a))

class DriverAssignmentService:
    def assign_driver(self, order: Order) -> Optional[Driver]:
        restaurant = db.get_restaurant(order.restaurant_id)
        available_drivers = db.get_available_drivers()   # status=available, no current order
        if not available_drivers:
            return None
        # Find nearest driver to restaurant
        nearest = min(
            available_drivers,
            key=lambda d: haversine_distance(
                d.latitude, d.longitude,
                restaurant.latitude, restaurant.longitude
            )
        )
        # Assign atomically
        success = db.assign_driver_if_available(nearest.driver_id, order.order_id)
        if not success:
            return self.assign_driver(order)   # retry if driver was just taken
        nearest.current_order_id = order.order_id
        nearest.is_available = False
        order.driver_id = nearest.driver_id
        transition(order, OrderStatus.DRIVER_ASSIGNED)
        notify_driver(nearest, order)
        notify_customer(order)
        return nearest

Real-Time Status Updates

Use WebSocket connections (Socket.IO or server-sent events) for real-time order tracking. On each status transition, emit an event to a Kafka topic (order-events). A notification service consumes the topic and:

  • Pushes status update to the customer’s WebSocket connection via a presence server (customer_id → connection mapping in Redis).
  • Sends push notification (FCM/APNs) if the customer’s app is backgrounded.
  • Updates the driver app’s current task.

ETA Calculation

def estimate_delivery(restaurant, driver, delivery_address) -> datetime:
    driver_to_restaurant_km = haversine_distance(
        driver.latitude, driver.longitude,
        restaurant.latitude, restaurant.longitude
    )
    restaurant_to_customer_km = haversine_distance(
        restaurant.latitude, restaurant.longitude,
        *geocode(delivery_address)
    )
    avg_speed_kmh = 25  # urban driving speed
    pickup_minutes = (driver_to_restaurant_km / avg_speed_kmh) * 60
    delivery_minutes = (restaurant_to_customer_km / avg_speed_kmh) * 60
    total = restaurant.average_prep_minutes + pickup_minutes + delivery_minutes
    return datetime.utcnow() + timedelta(minutes=total)

Interview Questions

Q: How do you handle the case where a driver is assigned but doesn’t pick up the order?

Set a pickup deadline: order must be picked up within N minutes of assignment (e.g., 15 minutes). A background job checks orders in DRIVER_ASSIGNED status past their deadline. If expired: unassign the driver (mark driver available), transition order back to READY, and re-run driver assignment. Track failed assignments per driver — multiple failures trigger account review. Notify the customer of the delay.

Q: How do you prevent duplicate orders when a customer double-taps Place Order?

The client generates a unique idempotency key (UUID) per order attempt and sends it in the request header. The server stores the key in a unique-constrained column alongside the order. If a duplicate key arrives within the TTL window (e.g., 24 hours), return the existing order without creating a new one. The database unique constraint prevents race conditions if two requests arrive simultaneously with the same key — only one INSERT succeeds.

{
“@context”: “https://schema.org”,
“@type”: “FAQPage”,
“mainEntity”: [
{
“@type”: “Question”,
“name”: “How do you design the order state machine for a food delivery system?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “States: PENDING (placed) u2192 CONFIRMED (restaurant accepted) u2192 PREPARING (kitchen started) u2192 READY (food done) u2192 DRIVER_ASSIGNED u2192 PICKED_UP u2192 DELIVERED. Parallel path: any state except PICKED_UP and DELIVERED can transition to CANCELLED. A VALID_TRANSITIONS dict enforces legal moves. On each transition, emit an event to a message queue (Kafka) u2014 consumers update the customer’s real-time tracking, send push notifications, and update analytics. The state machine prevents invalid operations like delivering before pickup.”
}
},
{
“@type”: “Question”,
“name”: “How do you assign drivers to orders efficiently?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Store driver locations in Redis (updated every 4 seconds via the driver app). On order READY: query available drivers (status=available, no current order) from Redis. Compute haversine distance from each driver to the restaurant. Assign the nearest driver using an atomic Redis command (SET driver:{id}:order_id {order_id} NX) to prevent race conditions u2014 two orders can’t grab the same driver. If the nearest driver is taken, retry with the next nearest. For high-volume markets, use a spatial index (Redis GEO commands: GEOSEARCH) to query only drivers within a radius.”
}
},
{
“@type”: “Question”,
“name”: “How do you handle driver no-shows after assignment?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Set a pickup deadline: order must be picked up within N minutes of assignment (typically 10-15 minutes). A background job polls orders in DRIVER_ASSIGNED status past their deadline. On expiry: (1) Unassign the driver u2014 mark driver available, clear current_order_id. (2) Transition order back to READY. (3) Re-run driver assignment. (4) Notify the customer of the delay. Track failed pickups per driver u2014 multiple failures in a week trigger account review. Repeated driver-side cancellations after assignment affect driver rating.”
}
},
{
“@type”: “Question”,
“name”: “How do you prevent duplicate orders from double-taps or retries?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “The client generates a UUID idempotency_key per order attempt and sends it as a request header. The server stores the key in a unique-constrained column in the orders table. On duplicate key: the SELECT before INSERT finds the existing order and returns it without creating a new one. For true concurrent protection, use a unique database constraint on idempotency_key u2014 if two requests race in simultaneously, only one INSERT succeeds; the other gets a unique constraint violation and retries the SELECT to return the existing order. Keys expire after 24 hours.”
}
},
{
“@type”: “Question”,
“name”: “How would you scale a food delivery system to millions of orders per day?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Shard orders by restaurant_id or city_id (most queries are geographically scoped). Use Redis for driver locations and order status hot data (fast read/write). Use Kafka for the event bus: order state changes u2192 notifications, analytics, driver app updates. The driver assignment service reads from Redis GEO for proximity queries rather than querying the database. Use a CDN for restaurant menus (change infrequently, high read volume). During dinner rush, the bottleneck is driver assignment u2014 precompute proximity scores when drivers go online, cache for fast lookup during peak demand.”
}
}
]
}

Asked at: DoorDash Interview Guide

Asked at: Uber Interview Guide

Asked at: Lyft Interview Guide

Asked at: Snap Interview Guide

Scroll to Top