Low-Level Design: Food Delivery Order Tracking — Real-time Location, State Machine, and ETA

Core Entities

Order: order_id, customer_id, restaurant_id, driver_id, items[], total_cents, status, placed_at, estimated_delivery_at. OrderStatus (state machine): PLACED -> CONFIRMED -> PREPARING -> READY_FOR_PICKUP -> PICKED_UP -> EN_ROUTE -> DELIVERED / CANCELLED. Driver: driver_id, current_location (lat/lng), status (AVAILABLE, ON_DELIVERY), current_order_id. TrackingUpdate: update_id, order_id, driver_location, timestamp, event_type (LOCATION_UPDATE, STATUS_CHANGE).

Order State Machine

Each transition is triggered by a specific actor:

  • PLACED -> CONFIRMED: restaurant accepts the order (or auto-confirm after 2 minutes)
  • CONFIRMED -> PREPARING: restaurant starts preparing
  • PREPARING -> READY_FOR_PICKUP: restaurant marks food as ready
  • READY_FOR_PICKUP -> PICKED_UP: driver confirms pickup at restaurant
  • PICKED_UP -> EN_ROUTE: driver departs restaurant (or combined with PICKED_UP)
  • EN_ROUTE -> DELIVERED: driver confirms delivery at customer location
  • Any state -> CANCELLED: customer or restaurant cancels (with rules per state)

Invalid transitions are rejected. Store current_status and status_history (for audit and customer UI timeline).

Real-time Location Tracking

Driver app sends GPS coordinates every 5 seconds while on delivery. WebSocket connection from the customer app to a Location Service receives live driver location. Architecture: Driver app -> POST /location (HTTP or WebSocket) -> Location Service stores in Redis (GEOADD active_drivers lng lat driver_id) and publishes to a channel (Redis Pub/Sub or Kafka topic per order). Customer app subscribes to the order channel and receives location updates. Location Service fans out to all subscribers for that order_id. If the customer loses connection, on reconnect the app gets the current driver location from Redis GEOPOS.

ETA Calculation

ETA = preparation_time_remaining + pickup_travel_time + delivery_travel_time. Components: preparation_time_remaining: estimated from restaurant history (average prep time for this restaurant at this time of day) minus time elapsed since PREPARING. pickup_travel_time: routing API call (Google Maps, Mapbox) from driver current location to restaurant. delivery_travel_time: routing API call from restaurant to customer. Update ETA every 30 seconds as driver location changes. Cache routing API results for (origin_geohash, dest_geohash, time_of_day) with 5-minute TTL to reduce API costs. Display ETA to the customer as a range (“arriving in 15-20 minutes”) to account for uncertainty.

Push Notifications for Status Changes

class OrderNotifier:
    STATUS_MESSAGES = {
        "CONFIRMED":         "Your order has been confirmed!",
        "PREPARING":         "Restaurant is preparing your food",
        "READY_FOR_PICKUP":  "Driver is picking up your order",
        "PICKED_UP":         "Your food is on the way!",
        "DELIVERED":         "Your order has been delivered. Enjoy!",
        "CANCELLED":         "Your order has been cancelled",
    }

    def on_status_change(self, order_id, new_status):
        order = self.db.get_order(order_id)
        message = self.STATUS_MESSAGES.get(new_status)
        if message:
            self.push.send(order.customer_id, message,
                           data={"order_id": order_id,
                                 "status": new_status})

Cancellation Rules

Customer can cancel: any time before PREPARING (full refund). After PREPARING: cancellation fee or no refund (restaurant has started cooking). After PICKED_UP: cannot cancel (driver is en route). Restaurant can cancel: before PICKED_UP (trigger driver reassignment or refund). Driver can cancel: before PICKED_UP (trigger driver reassignment). Store cancellation_reason and cancelled_by for auditing. On cancellation: process refund via payment service, notify the other parties, release driver (set status to AVAILABLE).

Scaling

At 1M concurrent active orders: 1M drivers sending location every 5 seconds = 200K writes/second to Redis. Use Redis Cluster. Customer WebSocket connections: use a WebSocket gateway (Socket.io cluster or AWS API Gateway WebSocket) that fans out to all subscribers for an order. Kafka for durable order event streaming (order state changes are events consumed by notification service, ETA service, analytics). Database: shard orders by order_id hash. Read replicas for customer-facing queries (order status, history).

Asked at: DoorDash Interview Guide

Asked at: Uber Interview Guide

Asked at: Lyft Interview Guide

Asked at: Snap Interview Guide

Scroll to Top