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.

Asked at: DoorDash Interview Guide

Asked at: Uber Interview Guide

Asked at: Lyft Interview Guide

Asked at: Shopify Interview Guide

Scroll to Top