Low-Level Design: Online Food Delivery (DoorDash/Uber Eats) — Order Lifecycle, Driver Assignment, and ETA

Core Entities

Restaurant: restaurant_id, name, address, lat, lng, cuisine_type, rating, prep_time_minutes (avg), is_active, operating_hours. MenuItem: item_id, restaurant_id, name, description, price, category, is_available, prep_time_minutes. Order: order_id, customer_id, restaurant_id, driver_id (nullable), status (PLACED, CONFIRMED, PREPARING, READY_FOR_PICKUP, PICKED_UP, DELIVERED, CANCELLED), items (JSON), subtotal, delivery_fee, tip, total, delivery_address, created_at, estimated_delivery_at. OrderItem: item_id, order_id, menu_item_id, quantity, unit_price, special_instructions. Driver: driver_id, name, phone, status (OFFLINE, AVAILABLE, ON_DELIVERY), current_lat, current_lng, vehicle_type. DeliveryZone: zone_id, polygon (geospatial), restaurant_id (zones where this restaurant delivers).

Order Lifecycle

Order state machine: PLACED (customer submitted order, payment authorized) → CONFIRMED (restaurant accepted, payment captured) → PREPARING (kitchen working on it) → READY_FOR_PICKUP (food ready, awaiting driver) → PICKED_UP (driver has the food) → DELIVERED (order complete). Cancellations: PLACED → CANCELLED (free cancellation before restaurant confirms). CONFIRMED/PREPARING → CANCELLED with partial refund (restaurant may have started work). READY_FOR_PICKUP → CANCELLED is rare (food already made). Each transition emits an event to a Kafka topic (order_events). Downstream consumers: notification service (SMS/push to customer and driver), ETA calculator (recalculate on each state change), analytics (funnel tracking). Restaurant confirmation: restaurant has N minutes to confirm (configurable, typically 5). If not confirmed: auto-cancel and notify customer. Restaurant app pings the order via WebSocket or polling. Driver assignment happens after CONFIRMED (not PLACED) to avoid assigning a driver to an order the restaurant might reject.

Driver Assignment

class DispatchService:
    def assign_driver(self, order: Order) -> Optional[Driver]:
        restaurant = self.repo.get_restaurant(order.restaurant_id)

        # Find available drivers near the restaurant
        nearby = self.geo.find_nearby_drivers(
            lat=restaurant.lat,
            lng=restaurant.lng,
            radius_km=5.0
        )

        def score(d: Driver) -> float:
            # Distance to restaurant + penalty for declining too much
            dist = haversine(restaurant.lat, restaurant.lng,
                             d.current_lat, d.current_lng)
            return dist + (1 - d.acceptance_rate) * 2.0

        candidates = sorted(nearby[:10], key=score)

        for driver in candidates:
            # Offer with 30-second timeout
            accepted = self.offer_order(driver, order, timeout=30)
            if accepted:
                order.driver_id = driver.driver_id
                order.status = OrderStatus.CONFIRMED
                return driver

        return None  # no driver available, re-queue for retry

ETA Calculation

Delivery ETA = restaurant prep time + driver travel to restaurant + driver travel to customer. Prep time: use the restaurant’s historical average (prep_time_minutes), adjusted for current order queue size. If the restaurant has 10 orders in PREPARING state: add buffer (each order adds ~2 min). Driver-to-restaurant ETA: compute using the routing API (Google Maps Distance Matrix or OSRM) with current traffic. Driver-to-customer ETA: compute after driver picks up (more accurate — we now know the actual pickup time). ETA update triggers: on CONFIRMED (initial estimate), on driver assignment (driver location known), on PICKED_UP (actual departure time known). Display to customer: show estimated delivery window (not exact time) to manage expectations. Update the displayed ETA in real time via WebSocket push as state transitions occur. ETA accuracy tracking: log predicted vs actual delivery time per order. Analyze by restaurant, driver, time of day. Use to tune the prep_time estimate and traffic model. Driver location: update Redis every 10 seconds from the driver app. GEORADIUS queries for driver matching. Track GPS breadcrumbs for post-delivery analysis.

{
“@context”: “https://schema.org”,
“@type”: “FAQPage”,
“mainEntity”: [
{
“@type”: “Question”,
“name”: “How does a food delivery app handle an order when the restaurant is not responding?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Restaurant confirmation timeout is a critical user experience issue. If a restaurant doesn’t confirm within the timeout (typically 5 minutes): (1) Auto-cancel and notify: the order is cancelled, the customer is notified (“Sorry, the restaurant is unable to accept your order right now”), and the payment is refunded immediately. This is the safest option — don’t leave the customer waiting. (2) Auto-accept: some platforms auto-confirm orders for partnered restaurants with reliable POS integration — the order is directly injected into the POS system without manual restaurant confirmation. Reduces cancellations. (3) Re-queue and retry: if the restaurant’s tablet was temporarily offline, retry the notification every 30 seconds for 5 minutes before cancelling. Implementation: when an order is PLACED, schedule a job to fire at T+5 minutes. The job checks: if order.status == PLACED: auto-cancel. If a restaurant accepts before T+5: the confirmation handler cancels the scheduled job (idempotency key on the job or a status check at execution time). Track restaurant confirmation rates: restaurants below 95% confirmation rate receive warnings; repeat offenders are paused.”
}
},
{
“@type”: “Question”,
“name”: “How do you calculate accurate delivery ETAs that account for restaurant prep time and traffic?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “ETA = time_now + restaurant_prep_time + driver_travel_to_restaurant + driver_travel_to_customer. Restaurant prep time: base estimate = restaurant.prep_time_minutes (historical average). Real-time adjustment: count how many orders are currently in PREPARING state for this restaurant. Each additional order adds ~2 minutes to the queue (configurable, calibrated per restaurant). Driver-to-restaurant: compute using routing API with live traffic (Google Maps Distance Matrix, Waze, or Mapbox). Use current driver location (updated every 10 seconds in Redis). For initial ETA (before driver assignment): estimate using the nearest available driver’s expected position. Driver-to-customer: not included until PICKED_UP (more accurate to use actual pickup time). After PICKED_UP: compute routing from driver’s pickup location to customer. ETA accuracy loop: log (estimated_delivery_time, actual_delivery_time) per order. Segment by restaurant, time of day, driver. Use statistical analysis (P50, P90 errors) to calibrate prep_time estimates and routing model biases. A well-tuned system should achieve P50 accuracy within u00b13 minutes. Communicate uncertainty: show a delivery window (25-35 min) rather than a single time (30 min) to manage expectations.”
}
},
{
“@type”: “Question”,
“name”: “How do you handle “driver poaching” — drivers accepting orders and then cancelling?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Driver accept-then-cancel (poaching) is when a driver accepts an order to see the destination or payout, then cancels if it’s not favorable. This harms restaurants (food gets cold) and customers (longer wait). Detection: high-cancellation drivers (>5% cancellations after acceptance) are flagged. Patterns: cancellations within 30 seconds of acceptance (destination check), or cancellations for orders with low tips. Disincentives: (1) Acceptance rate impacts earnings: drivers with acceptance_rate < 70% receive fewer order offers (deprioritized in matching). (2) Cancel-after-accept penalty: a cancellation after acceptance (especially PREPARING or later) reduces the driver's earnings score. Multiple cancellations in a session = temporary suspension. (3) Upfront destination visibility: show the approximate drop-off area (not exact address) before acceptance — reduces cancellations for destination reasons. (4) Tip visibility: in markets where tips are shown upfront (opt-in), drivers can filter by tip amount at offer time, reducing post-accept cancellations. Severe cases (fraud): drivers who consistently accept-and-cancel with no deliveries can trigger the anti-fraud system for account review."
}
},
{
"@type": "Question",
"name": "How do you design the real-time order tracking map for customers?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Customers track their delivery in real time on a map showing the driver's location. Architecture: (1) Driver location updates: driver app sends GPS coordinates every 10 seconds to a location service (REST or gRPC). Location service: UPDATE drivers SET lat=:lat, lng=:lng, updated_at=NOW() WHERE driver_id=:id, AND update Redis: GEOADD driver_locations lng lat driver_id. (2) Customer subscription: customer app opens a WebSocket connection to a tracking server. Registers interest in their order: subscribe("order:{order_id}"). (3) Location broadcast: a Kafka consumer (location events topic) receives driver location updates. For each update: check if the driver has an active order (lookup in Redis: driver:{id}:current_order). If yes: publish to the WebSocket channel "order:{order_id}" with the new coordinates. (4) Client rendering: the customer app smoothly animates the driver marker between GPS updates (interpolation). Shows estimated minutes remaining (recalculated on each location update). (5) Privacy: don't show exact driver location until PICKED_UP. Before pickup: show a general "driver is nearby" indicator. After DELIVERED: stop broadcasting driver location."
}
},
{
"@type": "Question",
"name": "How do you handle restaurant menus with frequent availability changes (items selling out)?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Menu availability changes frequently: an item sells out mid-service, a kitchen runs out of an ingredient, a seasonal item goes off-menu. Update pipeline: restaurant staff marks an item as unavailable via the tablet app u2192 POST /menu-items/:id {is_available: false} u2192 database update u2192 invalidate menu cache for this restaurant u2192 push update to active ordering sessions. Real-time invalidation: customers currently browsing the menu should see the change within seconds. Architecture: menu is cached in Redis per restaurant (TTL 60 seconds). On availability change: REDIS DEL menu:{restaurant_id}. A publish-subscribe notification (Redis Pub/Sub or WebSocket channel) tells active sessions to refresh. If a customer adds an unavailable item to their cart and attempts checkout: server validates all items at checkout, rejects unavailable items with a clear error ("Sorry, the Spicy Tuna Roll is no longer available"). Customer is prompted to remove the item or substitute. Menu sync for high-volume: restaurants with POS integrations push menu updates automatically. The platform pulls menu snapshots every 5 minutes as a fallback for restaurants without webhook support. Batch sync at business open: menus are re-synced at restaurant opening time to reset availabilities from the previous day."
}
}
]
}

Asked at: DoorDash Interview Guide

Asked at: Uber Interview Guide

Asked at: Lyft Interview Guide

Asked at: Snap Interview Guide

Scroll to Top