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