Low-Level Design: Food Ordering System (DoorDash/UberEats) — Orders, Dispatch, and Delivery Tracking

Core Entities

Restaurant: restaurant_id, name, address, lat, lng, cuisine_type, rating, is_open, prep_time_minutes (average). MenuItem: item_id, restaurant_id, name, description, price, category, is_available, calories. Order: order_id, customer_id, restaurant_id, dasher_id, status (PLACED, ACCEPTED, PREPARING, READY_FOR_PICKUP, PICKED_UP, DELIVERED, CANCELLED), items (JSON), subtotal, delivery_fee, tax, tip, delivery_address, placed_at, estimated_delivery_at. Dasher: dasher_id, name, phone, current_lat, current_lng, status (AVAILABLE, ON_DELIVERY), vehicle_type, rating. DeliveryZone: zone_id, polygon (geospatial), active_dashers_count.

Order Placement Flow

class OrderService:
    def place_order(self, customer_id, restaurant_id, items, delivery_address):
        # 1. Validate items are available
        menu_items = self.menu_repo.get_items([i.item_id for i in items])
        for item in menu_items:
            if not item.is_available:
                raise ItemUnavailableError(item.item_id)

        # 2. Compute pricing
        subtotal = sum(i.quantity * item.price for i, item in zip(items, menu_items))
        delivery_fee = self.pricing.compute_delivery_fee(restaurant_id, delivery_address)
        tax = subtotal * TAX_RATE
        estimated_prep = self.restaurant_repo.get_prep_time(restaurant_id)
        estimated_delivery = datetime.now() + timedelta(minutes=estimated_prep + 20)

        # 3. Charge customer (pre-authorization)
        payment_intent = self.payment.authorize(customer_id, subtotal + delivery_fee + tax)

        # 4. Create order
        order = Order(customer_id=customer_id, restaurant_id=restaurant_id,
                      items=items, status=OrderStatus.PLACED,
                      payment_intent_id=payment_intent.id,
                      estimated_delivery_at=estimated_delivery)
        self.order_repo.save(order)

        # 5. Notify restaurant
        self.notification.notify_restaurant(restaurant_id, order)
        return order

Dasher Dispatch

When the restaurant marks the order as READY_FOR_PICKUP, the dispatch system assigns a dasher. Dispatch algorithm: (1) Find available dashers within 5km of the restaurant. Query Redis geo index: GEORADIUS dasher_locations :restaurant_lat :restaurant_lng 5 km WITHCOORD COUNT 20. (2) For each candidate dasher: compute score = distance_weight * distance + wait_time_weight * time_since_last_order. Lower score = better candidate. (3) Offer to the best-scoring dasher. The dasher has 30 seconds to accept. If declined or no response: offer to the next candidate. (4) On acceptance: update Dasher.status = ON_DELIVERY, update Order.dasher_id, send pickup ETA to the customer. Batching: a dasher can carry multiple orders from the same restaurant or orders from nearby restaurants along the delivery route (increases dasher earnings, reduces per-order cost).

Real-Time Delivery Tracking

Dashers send location updates every 3-5 seconds from the mobile app. Server: receive location update → update Redis GEOPOS dasher_locations :dasher_id :lat :lng → publish to a Kafka topic dasher_location_updates partitioned by dasher_id. The order service subscribes to updates for orders in transit. For each location update: recompute the ETA using a routing API (Google Maps, Mapbox). If ETA changed by > 3 minutes: push an updated ETA notification to the customer via FCM/APNs. Customer-facing tracking: the customer’s app subscribes to a WebSocket channel scoped to their order. The server pushes location updates and status changes to the channel. WebSocket connection per active order: with 1M concurrent deliveries, that’s 1M open WebSocket connections — manageable with a connection server layer (similar to the chat system design).

Order Status State Machine

Valid transitions: PLACED → ACCEPTED (restaurant confirms) or CANCELLED (timeout). ACCEPTED → PREPARING → READY_FOR_PICKUP. READY_FOR_PICKUP → PICKED_UP (dasher confirms pickup). PICKED_UP → DELIVERED (dasher confirms delivery). Any state → CANCELLED (before PICKED_UP, with different refund rules). Invalid transitions are rejected by the service layer. Store status transitions in an order_events log table for the customer timeline view and customer support debugging. Each transition is atomic: update order status + emit an event to Kafka in the same database transaction (outbox pattern).

Search and Discovery

Restaurant search: full-text on name and cuisine (Elasticsearch). Filter by: delivery zone (PostGIS: does the delivery address fall within the restaurant’s delivery polygon?), is_open (check current time against restaurant hours), min_order_amount. Sort by: rating, estimated delivery time, promotional rank (paid placement). Elasticsearch documents include pre-computed delivery_time_estimate (restaurant prep time + estimated dasher travel time to the delivery address). Update this estimate periodically based on real delivery data. Autocomplete for cuisine search: Trie or Elasticsearch completion suggester. Most popular restaurants are cached in Redis by delivery zone.

{
“@context”: “https://schema.org”,
“@type”: “FAQPage”,
“mainEntity”: [
{
“@type”: “Question”,
“name”: “How do you dispatch the best dasher for an order in real time?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Dasher dispatch must complete in under a second. Steps: (1) Spatial query: use Redis GEORADIUS or PostGIS ST_DWithin to find all available dashers within 5km of the restaurant. Dashers send location pings every 5 seconds; their positions are stored in Redis geo index. (2) Score candidates: for each candidate dasher, compute a score based on distance to the restaurant, dasher rating, time since last order (to balance earnings), and vehicle type (bike dashers for short distances, cars for long). (3) Offer: send an offer notification to the top-scoring dasher via push notification. Timeout: 30 seconds. On decline or timeout, offer to the next candidate. (4) Confirmation: when accepted, lock the assignment in the database (optimistic locking — check dasher is still AVAILABLE before committing). This prevents two orders from being assigned to the same dasher simultaneously.”
}
},
{
“@type”: “Question”,
“name”: “How do you estimate delivery time accurately?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Delivery time = restaurant prep time + dasher pickup travel time + delivery travel time. Restaurant prep time: historical average per restaurant and time of day (peak hours have longer prep). Updated with exponential moving average from recent actual prep times. Dasher pickup travel time: routing API (Google Maps, Mapbox) from dasher’s current location to the restaurant, adjusted for current traffic. Delivery travel time: routing API from restaurant to delivery address. Machine learning refinement: a model trained on historical deliveries predicts the final ETA more accurately than routing APIs alone (accounts for parking, building access time, elevator waits, dasher behavior). The ML model improves continuously with each completed delivery. Display to the customer: round to the nearest 5 minutes and show a range (25-35 min) to set expectations appropriately.”
}
},
{
“@type”: “Question”,
“name”: “How does order batching work for dashers?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Order batching assigns multiple orders to one dasher to improve efficiency. A dasher picks up order A from restaurant X, picks up order B from nearby restaurant Y (or the same restaurant), delivers A, delivers B. Constraints: the second pickup must not delay the first delivery by more than N minutes (typically 10-15 min). The combined route must be efficient (no excessive backtracking). The delivery system evaluates batching candidates when a dasher is assigned: is there another nearby unassigned order at the same restaurant or within 0.5km, with a similar delivery direction? If yes and the route is feasible: bundle the orders. The dasher sees a stacked order notification. Dashers earn a bonus per stacked delivery. Batching reduces per-order delivery cost by 20-30% and is critical for profitability.”
}
},
{
“@type”: “Question”,
“name”: “How do you handle a restaurant being slow or cancelling an order?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Restaurant is slow (prep time exceeded): the system tracks the time since ACCEPTED status. If prep time exceeds 2x the estimated prep time, send an alert to the restaurant. If a dasher is already waiting at the restaurant, notify them of the delay. Update the customer’s ETA. If the delay exceeds 30 minutes beyond the original ETA: proactively offer the customer a coupon or the option to cancel for a full refund. Restaurant cancels order: the restaurant can cancel before PICKED_UP. This triggers: release the payment authorization, refund the customer immediately, notify the customer with an apology and a coupon, attempt to reassign any waiting dasher to another order. Track restaurant cancellation rate — high-cancellation restaurants are penalized in search rankings and flagged for account review. Restaurant reliability is a key metric (affects customer satisfaction and dasher efficiency).”
}
},
{
“@type”: “Question”,
“name”: “How does the food ordering platform handle peak load during lunch and dinner rushes?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Traffic is highly predictable: 11am-2pm and 6pm-9pm. Pre-scale compute: auto-scaling groups with a scheduled scaling policy (add N instances at 10:30am, scale down at 3pm). Pre-warm the Redis cache with popular restaurant data for each delivery zone before the rush. Dasher dispatch queue: during peak, dispatch may be delayed as dashers are all on deliveries. Queue unmatched orders; dispatch as dashers complete deliveries. ETA padding: during peak, add a buffer to estimated delivery times (be honest about delays rather than promising impossible ETAs). Database: read replicas handle the read-heavy order status and restaurant search queries. Writes go to the primary. Connection pooling (PgBouncer) handles the connection burst from many simultaneous order placements. Chaos testing before major holidays (Super Bowl, Valentine’s Day) validates the system handles 3-5x normal load.”
}
}
]
}

Asked at: DoorDash Interview Guide

Asked at: Uber Interview Guide

Asked at: Lyft Interview Guide

Asked at: Shopify Interview Guide

Scroll to Top