Low-Level Design: Food Delivery App (DoorDash/Uber Eats) OOP Design

Low-Level Design: Food Delivery App (DoorDash / Uber Eats)

The food delivery app LLD question asks you to model the core entities and interactions of a platform like DoorDash, Uber Eats, or Grubhub. It tests OOP modelling, state machines, observer pattern for real-time updates, and strategy pattern for delivery assignment.

Requirements

  • Customers browse restaurants and place orders.
  • Restaurants receive, prepare, and mark orders ready.
  • Dashers (delivery drivers) pick up and deliver orders.
  • Real-time order status updates to the customer.
  • Support multiple delivery assignment strategies (nearest dasher, highest rating).

Order State Machine

from enum import Enum, auto

class OrderStatus(Enum):
    PENDING           = auto()   # order placed, awaiting restaurant accept
    ACCEPTED          = auto()   # restaurant confirmed
    PREPARING         = auto()   # kitchen working
    READY_FOR_PICKUP  = auto()   # food ready
    PICKED_UP         = auto()   # dasher has the food
    DELIVERED         = auto()   # customer received
    CANCELLED         = auto()   # cancelled at any stage before pickup

VALID_TRANSITIONS = {
    OrderStatus.PENDING:          {OrderStatus.ACCEPTED, OrderStatus.CANCELLED},
    OrderStatus.ACCEPTED:         {OrderStatus.PREPARING, OrderStatus.CANCELLED},
    OrderStatus.PREPARING:        {OrderStatus.READY_FOR_PICKUP, OrderStatus.CANCELLED},
    OrderStatus.READY_FOR_PICKUP: {OrderStatus.PICKED_UP},
    OrderStatus.PICKED_UP:        {OrderStatus.DELIVERED},
    OrderStatus.DELIVERED:        set(),
    OrderStatus.CANCELLED:        set(),
}

Core Domain Classes

from dataclasses import dataclass, field
from typing import Callable
import uuid, time

@dataclass
class Location:
    lat: float
    lng: float

    def distance_km(self, other: 'Location') -> float:
        # Haversine simplified for interview
        return ((self.lat - other.lat)**2 + (self.lng - other.lng)**2) ** 0.5 * 111

@dataclass
class MenuItem:
    item_id:     str
    name:        str
    price_cents: int
    available:   bool = True

@dataclass
class Restaurant:
    restaurant_id: str
    name:          str
    location:      Location
    menu:          dict[str, MenuItem] = field(default_factory=dict)

    def add_item(self, item: MenuItem) -> None:
        self.menu[item.item_id] = item

@dataclass
class OrderItem:
    menu_item: MenuItem
    quantity:  int

    @property
    def subtotal(self) -> int:
        return self.menu_item.price_cents * self.quantity

@dataclass
class Customer:
    customer_id: str
    name:        str
    location:    Location

@dataclass
class Dasher:
    dasher_id: str
    name:      str
    location:  Location
    available: bool = True
    rating:    float = 5.0

Order Class with Observer Pattern

class Order:
    def __init__(self, customer: Customer, restaurant: Restaurant,
                 items: list[OrderItem]):
        self.order_id    = str(uuid.uuid4())[:8]
        self.customer    = customer
        self.restaurant  = restaurant
        self.items       = items
        self.status      = OrderStatus.PENDING
        self.dasher: Dasher | None = None
        self.created_at  = time.time()
        self._observers: list[Callable] = []

    @property
    def total_cents(self) -> int:
        return sum(item.subtotal for item in self.items)

    def subscribe(self, callback: Callable) -> None:
        self._observers.append(callback)

    def _notify(self) -> None:
        for cb in self._observers:
            cb(self.order_id, self.status)

    def transition(self, new_status: OrderStatus) -> None:
        if new_status not in VALID_TRANSITIONS[self.status]:
            raise ValueError(
                f"Invalid transition: {self.status.name} -> {new_status.name}"
            )
        self.status = new_status
        self._notify()

    def assign_dasher(self, dasher: Dasher) -> None:
        self.dasher          = dasher
        dasher.available     = False

    def __repr__(self) -> str:
        total = self.total_cents / 100
        return (f"Order({self.order_id}) from {self.restaurant.name} "
                f"| status={self.status.name} | total=$" + f"{total:.2f}")

Platform / Delivery Assignment (Strategy Pattern)

from abc import ABC, abstractmethod

class AssignmentStrategy(ABC):
    @abstractmethod
    def select_dasher(self, dashers: list[Dasher],
                      order: Order) -> Dasher | None:
        ...

class NearestDasherStrategy(AssignmentStrategy):
    def select_dasher(self, dashers, order):
        available = [d for d in dashers if d.available]
        if not available:
            return None
        return min(available,
                   key=lambda d: d.location.distance_km(order.restaurant.location))

class HighestRatedDasherStrategy(AssignmentStrategy):
    def select_dasher(self, dashers, order):
        available = [d for d in dashers if d.available]
        if not available:
            return None
        return max(available, key=lambda d: d.rating)

class FoodDeliveryPlatform:
    def __init__(self, strategy: AssignmentStrategy | None = None):
        self.restaurants: dict[str, Restaurant] = {}
        self.customers:   dict[str, Customer]   = {}
        self.dashers:     dict[str, Dasher]     = {}
        self.orders:      dict[str, Order]      = {}
        self.strategy = strategy or NearestDasherStrategy()

    def register_restaurant(self, restaurant: Restaurant) -> None:
        self.restaurants[restaurant.restaurant_id] = restaurant

    def register_dasher(self, dasher: Dasher) -> None:
        self.dashers[dasher.dasher_id] = dasher

    def register_customer(self, customer: Customer) -> None:
        self.customers[customer.customer_id] = customer

    def place_order(self, customer_id: str, restaurant_id: str,
                    item_quantities: dict[str, int]) -> Order:
        customer   = self.customers[customer_id]
        restaurant = self.restaurants[restaurant_id]
        items = [
            OrderItem(restaurant.menu[iid], qty)
            for iid, qty in item_quantities.items()
            if iid in restaurant.menu and restaurant.menu[iid].available
        ]
        if not items:
            raise ValueError("No valid items in order")
        order = Order(customer, restaurant, items)
        order.subscribe(self._on_status_change)
        self.orders[order.order_id] = order
        print(f"Order placed: {order}")
        return order

    def accept_order(self, order_id: str) -> None:
        order = self.orders[order_id]
        order.transition(OrderStatus.ACCEPTED)
        dasher = self.strategy.select_dasher(list(self.dashers.values()), order)
        if dasher:
            order.assign_dasher(dasher)
            print(f"Dasher assigned: {dasher.name} for order {order_id}")

    def mark_preparing(self, order_id: str) -> None:
        self.orders[order_id].transition(OrderStatus.PREPARING)

    def mark_ready(self, order_id: str) -> None:
        self.orders[order_id].transition(OrderStatus.READY_FOR_PICKUP)

    def mark_picked_up(self, order_id: str) -> None:
        self.orders[order_id].transition(OrderStatus.PICKED_UP)

    def mark_delivered(self, order_id: str) -> None:
        order = self.orders[order_id]
        order.transition(OrderStatus.DELIVERED)
        if order.dasher:
            order.dasher.available = True

    def _on_status_change(self, order_id: str, status: OrderStatus) -> None:
        print(f"[Notification] Order {order_id} -> {status.name}")

Usage

platform = FoodDeliveryPlatform(strategy=NearestDasherStrategy())

# Setup
r = Restaurant("r1", "Pizza Palace", Location(37.77, -122.41))
r.add_item(MenuItem("p1", "Margherita", 1200))
r.add_item(MenuItem("p2", "Pepperoni",  1500))
platform.register_restaurant(r)

platform.register_customer(Customer("c1", "Alice", Location(37.78, -122.41)))
platform.register_dasher(Dasher("d1", "Bob", Location(37.76, -122.40)))

# Full order lifecycle
order = platform.place_order("c1", "r1", {"p1": 2, "p2": 1})
platform.accept_order(order.order_id)
platform.mark_preparing(order.order_id)
platform.mark_ready(order.order_id)
platform.mark_picked_up(order.order_id)
platform.mark_delivered(order.order_id)

Design Patterns Used

Pattern Usage
State machine OrderStatus with valid transition map prevents illegal state changes
Observer Order notifies subscribers (customer app, tracking dashboard) on every status change
Strategy AssignmentStrategy decouples dasher selection algorithm from platform logic
Repository Platform acts as an in-memory repository for restaurants, dashers, orders

Interview Extensions

How would you handle order cancellation mid-preparation?

CANCELLED is valid from PENDING, ACCEPTED, and PREPARING. If a dasher was already assigned, set dasher.available=True on cancel. If the restaurant already started cooking, apply a partial refund policy based on how far the state machine progressed. Emit a CANCELLED event to trigger customer notification and dasher release.

How would you add real-time location tracking for dashers?

Dasher mobile app pushes GPS coordinates every 5 seconds via WebSocket. The platform updates dasher.location in-memory (or Redis for distributed setup). Customer subscribes to a location stream for their order’s dasher. Use a Pub/Sub channel per order ID — dasher publishes, customer client subscribes.

How would you scale to millions of concurrent orders?

Partition orders by region (city-level). Each region has its own service instance with a separate pool of dashers and restaurants. Orders never cross region boundaries. Use a message queue (Kafka) for order events, a Redis sorted set for dasher availability by geohash, and a distributed state store (DynamoDB) for order state persistence.

{
“@context”: “https://schema.org”,
“@type”: “FAQPage”,
“mainEntity”: [
{
“@type”: “Question”,
“name”: “What design patterns are essential in a food delivery app LLD?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “State machine for order lifecycle (PENDING u2192 ACCEPTED u2192 PREPARING u2192 READY u2192 PICKED_UP u2192 DELIVERED/CANCELLED). Observer pattern for real-time status notifications to customers. Strategy pattern for dasher assignment (nearest, highest-rated, lowest current load). Repository pattern for managing restaurants, orders, and dashers as in-memory or persistent stores.”
}
},
{
“@type”: “Question”,
“name”: “How do you model the order state machine in OOP?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Define an OrderStatus enum with all states. Maintain a VALID_TRANSITIONS dict mapping each status to its set of valid next statuses. In Order.transition(), validate the new status is in VALID_TRANSITIONS[current_status] before updating, otherwise raise ValueError. This enforces the state machine at the class level and makes illegal transitions impossible.”
}
},
{
“@type”: “Question”,
“name”: “How would you assign dashers to orders efficiently at scale?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “In production: index available dashers in a geospatial store (Redis GEOSEARCH or PostGIS). When an order is placed, query dashers within a radius sorted by distance. Apply business rules (rating threshold, current load). Dispatch via a message queue (Kafka) to avoid double-assignment. For LLD interviews, the Strategy pattern with a nearest-dasher implementation demonstrates the right abstractions.”
}
},
{
“@type”: “Question”,
“name”: “How would you handle dasher unavailability or order cancellation mid-delivery?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Define a re-assignment flow: if a dasher drops an order before PICKED_UP, transition order back to READY_FOR_PICKUP and trigger the assignment strategy again. If cancelled after PICKED_UP, handle via an exception flow with refund. Use compensation events in an event-sourced system: each state transition is an immutable event, enabling replay and audit of the full order history.”
}
},
{
“@type”: “Question”,
“name”: “How does the Observer pattern improve a food delivery system?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “The Observer pattern decouples order state changes from notification channels. The Order object maintains a list of subscriber callbacks. On every status transition, it calls all subscribers with (order_id, new_status). Subscribers can include: customer push notification service, restaurant dashboard, dasher app, analytics pipeline. Adding a new notification channel requires only registering a new observer u2014 no changes to Order logic.”
}
}
]
}

Asked at: DoorDash Interview Guide

Asked at: Uber Interview Guide

Asked at: Lyft Interview Guide

Asked at: Shopify Interview Guide

Asked at: Stripe Interview Guide

Scroll to Top