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.

Asked at: DoorDash Interview Guide

Asked at: Uber Interview Guide

Asked at: Lyft Interview Guide

Asked at: Snap Interview Guide

Scroll to Top