Low-Level Design: Food Delivery Platform — Order Flow, Driver Dispatch, and Real-Time Tracking

Core Entities

Customer: customer_id, name, email, phone, default_address, stripe_customer_id. Restaurant: restaurant_id, name, address, location (lat/lng), cuisine_type, rating, is_open, prep_time_minutes, operating_hours. MenuItem: item_id, restaurant_id, name, description, price, category, is_available, photo_url. Order: order_id, customer_id, restaurant_id, driver_id, status (PLACED, ACCEPTED, PREPARING, READY, PICKED_UP, DELIVERED, CANCELLED), items (JSON), subtotal, delivery_fee, tip, total, delivery_address, special_instructions, placed_at, accepted_at, ready_at, picked_up_at, delivered_at. Driver: driver_id, name, phone, vehicle_type, status (OFFLINE, ONLINE, ON_DELIVERY), current_location (lat/lng), last_location_update. DriverLocation: driver_id, lat, lng, heading, speed, timestamp (time-series, not persisted long-term). Review: review_id, order_id, customer_id, restaurant_rating (1-5), driver_rating (1-5), comment, created_at.

Order State Machine

class OrderService:
    VALID_TRANSITIONS = {
        "PLACED":     ["ACCEPTED", "CANCELLED"],
        "ACCEPTED":   ["PREPARING", "CANCELLED"],
        "PREPARING":  ["READY"],
        "READY":      ["PICKED_UP"],
        "PICKED_UP":  ["DELIVERED"],
        "DELIVERED":  [],
        "CANCELLED":  [],
    }

    def transition_order(self, order_id: int, new_status: str,
                         actor_id: int, actor_type: str) -> Order:
        with db.transaction():
            order = db.query(
                "SELECT * FROM orders WHERE order_id = %s FOR UPDATE", order_id
            )
            if new_status not in self.VALID_TRANSITIONS[order.status]:
                raise InvalidTransition(
                    f"{order.status} -> {new_status} not allowed"
                )
            timestamp_col = {
                "ACCEPTED":  "accepted_at",
                "READY":     "ready_at",
                "PICKED_UP": "picked_up_at",
                "DELIVERED": "delivered_at",
                "CANCELLED": "cancelled_at",
            }.get(new_status)

            update = {"status": new_status}
            if timestamp_col:
                update[timestamp_col] = "NOW()"

            db.update("orders", update, where={"order_id": order_id})
            self.notify_parties(order, new_status)
            return db.get_order(order_id)

Driver Dispatch

When an order is READY (or slightly before, based on prep time estimate): find the best available driver. Geospatial query: drivers store current_location in a PostGIS geography column or Redis geospatial index (GEOADD/GEORADIUS). Query available drivers within radius R (e.g., 5km) sorted by distance. Driver scoring: score = w1 * distance + w2 * estimated_delivery_time + w3 * driver_rating. Distance: straight-line (haversine) approximation; use routing API (Google Maps Distance Matrix) for accurate ETA at assignment time. Offer flow: send the assignment offer to the top-scored driver via push notification. Driver has 30 seconds to accept. If declined or timeout: remove driver from candidates and try the next. Repeat until accepted or no drivers available (escalate alert). Batch dispatch: for efficiency, group nearby orders and offer them together to one driver (multi-restaurant pickup), reducing per-order delivery cost.

Real-Time Location Tracking

Drivers send location updates every 3-5 seconds via a WebSocket connection or mobile SDK. Updates flow: Driver app → WebSocket gateway → Kafka topic (driver_locations). A location consumer processes updates: updates Redis GEOADD (for dispatch queries) and publishes to a customer-facing WebSocket channel for live map updates. Customers receive driver location via WebSocket: connect to wss://api.example.com/orders/{id}/track. On each driver location event: push lat/lng/ETA to all subscribers for that order. ETA recalculation: on each location update, call the routing API (or a lightweight ETA model) and push updated ETA to the customer. Persist only final delivery location (not every update) to the database — location history is too high-volume for a relational DB. Use a time-series store (InfluxDB, TimescaleDB) or discard after 24 hours.

Estimated Delivery Time (EDT) Prediction

EDT = prep_time + driver_travel_time + buffer. Prep time: restaurant-specific estimate based on order complexity and historical data. Use average prep time + percentile buffer (e.g., p75 of historical prep times for this restaurant at this hour). Driver travel time: restaurant → customer distance / average speed, or routing API call. Factors: time of day (rush hour), weather (reduce speed estimate by 30% in rain). ML approach: train a gradient boosted model on historical delivery data with features: restaurant, order size, time of day, driver vehicle type, distance, weather, day of week. Predicted p50 and p90 delivery time. Show customers a range: “35-50 minutes” rather than a point estimate — more honest and reduces support contacts from customers who see the exact estimate slip by 2 minutes.


{“@context”:”https://schema.org”,”@type”:”FAQPage”,”mainEntity”:[{“@type”:”Question”,”name”:”How does driver dispatch select and assign the optimal driver for an order?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Dispatch scoring: find all ONLINE drivers without an active delivery within radius R (5km). For each candidate driver: compute a score based on straight-line distance to the restaurant (primary factor), estimated arrival time accounting for traffic, driver rating, vehicle type suitability (e.g., bicycle for short urban orders), and recent acceptance rate (penalize drivers who frequently decline). Sort by score. Offer to the top driver: send push notification with order details and 30-second timeout. If declined or timed out: move to the next candidate. Persistence: record each offer attempt in a dispatch_attempts table for debugging and driver behavior analysis. For high-demand periods: pre-position drivers near restaurants with historically high order volumes.”}},{“@type”:”Question”,”name”:”How do you handle the case where no drivers are available for an order?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Escalating strategy: (1) Expand search radius (5km → 10km → 15km) and retry dispatch. (2) Increase dispatch priority (offer higher pay to incentivize acceptance). (3) Alert the restaurant to hold the food — pause the ready notification. (4) After a configured timeout (e.g., 15 minutes after READY): automatically cancel the order with a full refund and notify the customer with an apology credit. (5) Notify operations team for manual intervention in high-value or repeated failure cases. Proactively: use surge pricing on delivery fees during high-demand periods to attract more drivers online. Predictive availability: if the model predicts low driver availability in 20 minutes (based on historical patterns + current online count), surface a longer estimated delivery time to new customers to set expectations before they place the order.”}},{“@type”:”Question”,”name”:”How is real-time driver location tracking implemented at scale?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Drivers send GPS updates every 3-5 seconds via a persistent WebSocket or mobile SDK (background location updates). At 100K active drivers, this is 20K-33K location updates per second. Architecture: driver app → WebSocket gateway (horizontally scaled, stateless) → Kafka topic (driver_locations) → parallel consumers. Consumer 1: update Redis GEOADD (for dispatch geospatial queries, expiry = 30s to remove stale drivers). Consumer 2: push to customer-facing WebSocket for the driver's current order. Customer WebSocket: each customer subscribed to an active order receives driver location updates pushed via Server-Sent Events or WebSocket. On each update: recompute ETA and push to customer. No polling — push-based architecture keeps latency low and reduces server load compared to client polling.”}},{“@type”:”Question”,”name”:”How do you handle order cancellation and refunds in a food delivery system?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Cancellation policy depends on order state: PLACED (restaurant not yet accepted): full refund, cancel immediately. ACCEPTED/PREPARING: partial refund (restaurant may have started prep). The cancellation window and refund percentage are restaurant-configurable. READY or later: no refund (food is made), unless the reason is operational (no driver available, restaurant error). Refund flow: mark order CANCELLED in the database. Issue a Stripe refund (stripe.refunds.create with the payment_intent_id). For partial refunds: specify the amount. Send cancellation notification to customer, restaurant, and driver (if assigned). If a driver was assigned: set driver.status back to ONLINE and notify via push. Idempotency: the cancel endpoint uses an idempotency key to prevent double-processing if the client retries.”}},{“@type”:”Question”,”name”:”How does the platform calculate and display estimated delivery time to customers?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”EDT = restaurant_prep_time + driver_pickup_travel + driver_delivery_travel + buffer. Prep time: use the restaurant's historical p75 prep time for orders of similar size at the current hour of day. Driver pickup travel: distance from nearest available driver to restaurant / average speed at this time (accounting for traffic patterns). Driver delivery travel: restaurant to customer address via routing API (or straight-line distance * 1.4 urban detour factor). Buffer: 5-10 minute buffer for parking, pickup wait, handoff. Display: show a range (e.g., "30-45 min") rather than a point estimate. Update EDT dynamically as the order progresses: when the driver picks up, recalculate based on actual driver location and route. If actual prep time exceeds estimate: push an updated EDT to the customer proactively rather than letting them watch the clock.”}}]}

See also: DoorDash Interview Prep

See also: Uber Interview Prep

See also: Lyft Interview Prep

Scroll to Top