Low-Level Design: Food Delivery System (OOP Interview)
The food delivery system (DoorDash/Uber Eats style) is a rich OOP design problem covering order management, restaurant menus, delivery assignment, and real-time status tracking. It combines state machine design, domain modeling, and the Observer pattern into one cohesive system.
Requirements
- Restaurants manage their menus (items, prices, availability)
- Customers browse menus, build a cart, and place orders
- Orders go through: PLACED → CONFIRMED → PREPARING → READY → PICKED_UP → DELIVERED
- Delivery driver assignment: nearest available driver picks up the order
- Real-time status notifications to customer
- Rating system for restaurant and driver after delivery
Core Data Models
from dataclasses import dataclass, field
from enum import Enum
from datetime import datetime
from typing import Optional
import uuid
class OrderStatus(Enum):
PLACED = "placed"
CONFIRMED = "confirmed"
PREPARING = "preparing"
READY = "ready"
PICKED_UP = "picked_up"
DELIVERED = "delivered"
CANCELLED = "cancelled"
class DriverStatus(Enum):
AVAILABLE = "available"
ON_DELIVERY = "on_delivery"
OFFLINE = "offline"
@dataclass
class MenuItem:
item_id: str
name: str
price: float
description: str
category: str
is_available: bool = True
@dataclass
class Restaurant:
restaurant_id: str
name: str
address: str
cuisine: str
menu: dict[str, MenuItem] = field(default_factory=dict)
rating: float = 0.0
total_ratings: int = 0
def add_item(self, item: MenuItem) -> None:
self.menu[item.item_id] = item
def get_available_menu(self) -> list[MenuItem]:
return [item for item in self.menu.values() if item.is_available]
def update_rating(self, new_rating: float) -> None:
self.total_ratings += 1
self.rating = ((self.rating * (self.total_ratings - 1)) + new_rating) / self.total_ratings
@dataclass
class CartItem:
menu_item: MenuItem
quantity: int
special_instructions: str = ""
@property
def subtotal(self) -> float:
return self.menu_item.price * self.quantity
@dataclass
class Cart:
customer_id: str
restaurant_id: str
items: list[CartItem] = field(default_factory=list)
def add_item(self, menu_item: MenuItem, quantity: int, instructions: str = "") -> None:
for cart_item in self.items:
if cart_item.menu_item.item_id == menu_item.item_id:
cart_item.quantity += quantity
return
self.items.append(CartItem(menu_item, quantity, instructions))
def remove_item(self, item_id: str) -> None:
self.items = [ci for ci in self.items if ci.menu_item.item_id != item_id]
def total(self) -> float:
return sum(ci.subtotal for ci in self.items)
def is_empty(self) -> bool:
return len(self.items) == 0
@dataclass
class Driver:
driver_id: str
name: str
phone: str
status: DriverStatus = DriverStatus.AVAILABLE
rating: float = 0.0
total_ratings: int = 0
lat: float = 0.0
lng: float = 0.0
def update_rating(self, new_rating: float) -> None:
self.total_ratings += 1
self.rating = ((self.rating * (self.total_ratings - 1)) + new_rating) / self.total_ratings
@dataclass
class Order:
order_id: str = field(default_factory=lambda: str(uuid.uuid4()))
customer_id: str = ""
restaurant_id: str = ""
driver_id: Optional[str] = None
items: list[CartItem] = field(default_factory=list)
status: OrderStatus = OrderStatus.PLACED
subtotal: float = 0.0
delivery_fee: float = 2.99
total: float = 0.0
placed_at: datetime = field(default_factory=datetime.now)
delivered_at: Optional[datetime] = None
status_history: list[tuple[OrderStatus, datetime]] = field(default_factory=list)
def __post_init__(self):
self.total = self.subtotal + self.delivery_fee
self.status_history.append((self.status, self.placed_at))
def transition_to(self, new_status: OrderStatus) -> None:
# Validate allowed transitions
allowed = {
OrderStatus.PLACED: [OrderStatus.CONFIRMED, OrderStatus.CANCELLED],
OrderStatus.CONFIRMED: [OrderStatus.PREPARING, OrderStatus.CANCELLED],
OrderStatus.PREPARING: [OrderStatus.READY],
OrderStatus.READY: [OrderStatus.PICKED_UP],
OrderStatus.PICKED_UP: [OrderStatus.DELIVERED],
}
if new_status not in allowed.get(self.status, []):
raise ValueError(f"Cannot transition from {self.status} to {new_status}")
self.status = new_status
self.status_history.append((new_status, datetime.now()))
if new_status == OrderStatus.DELIVERED:
self.delivered_at = datetime.now()
Notification Service (Observer Pattern)
from abc import ABC, abstractmethod
class OrderObserver(ABC):
@abstractmethod
def on_status_change(self, order: Order) -> None:
pass
class CustomerNotifier(OrderObserver):
def on_status_change(self, order: Order) -> None:
messages = {
OrderStatus.CONFIRMED: "Your order has been confirmed!",
OrderStatus.PREPARING: "The restaurant is preparing your food.",
OrderStatus.READY: "Your order is ready for pickup.",
OrderStatus.PICKED_UP: "Your driver has picked up your order.",
OrderStatus.DELIVERED: "Your order has been delivered. Enjoy!",
OrderStatus.CANCELLED: "Your order has been cancelled.",
}
msg = messages.get(order.status)
if msg:
print(f"[SMS] Customer {order.customer_id}: {msg}")
class DriverNotifier(OrderObserver):
def on_status_change(self, order: Order) -> None:
if order.status == OrderStatus.READY and order.driver_id:
print(f"[PUSH] Driver {order.driver_id}: Order ready for pickup at restaurant.")
Delivery System (Main Service)
import math
class DeliveryService:
def __init__(self):
self._restaurants: dict[str, Restaurant] = {}
self._drivers: dict[str, Driver] = {}
self._orders: dict[str, Order] = {}
self._carts: dict[str, Cart] = {} # customer_id → Cart
self._observers: list[OrderObserver] = [
CustomerNotifier(), DriverNotifier()
]
def _notify(self, order: Order) -> None:
for observer in self._observers:
observer.on_status_change(order)
# ── Restaurant ─────────────────────────────────────────────────────────────
def register_restaurant(self, name: str, address: str, cuisine: str) -> Restaurant:
r = Restaurant(restaurant_id=str(uuid.uuid4()), name=name,
address=address, cuisine=cuisine)
self._restaurants[r.restaurant_id] = r
return r
# ── Cart ───────────────────────────────────────────────────────────────────
def get_or_create_cart(self, customer_id: str, restaurant_id: str) -> Cart:
cart = self._carts.get(customer_id)
if cart and cart.restaurant_id != restaurant_id:
raise ValueError("Cannot mix items from different restaurants")
if not cart:
cart = Cart(customer_id=customer_id, restaurant_id=restaurant_id)
self._carts[customer_id] = cart
return cart
def add_to_cart(self, customer_id: str, restaurant_id: str,
item_id: str, quantity: int = 1) -> Cart:
restaurant = self._restaurants.get(restaurant_id)
if not restaurant:
raise ValueError("Restaurant not found")
item = restaurant.menu.get(item_id)
if not item or not item.is_available:
raise ValueError(f"Item {item_id} not available")
cart = self.get_or_create_cart(customer_id, restaurant_id)
cart.add_item(item, quantity)
return cart
# ── Order ──────────────────────────────────────────────────────────────────
def place_order(self, customer_id: str) -> Order:
cart = self._carts.get(customer_id)
if not cart or cart.is_empty():
raise ValueError("Cart is empty")
order = Order(
customer_id=customer_id,
restaurant_id=cart.restaurant_id,
items=cart.items[:],
subtotal=cart.total(),
)
order.total = order.subtotal + order.delivery_fee
self._orders[order.order_id] = order
del self._carts[customer_id] # clear cart after order
self._notify(order)
return order
def update_order_status(self, order_id: str, new_status: OrderStatus) -> Order:
order = self._get_order(order_id)
order.transition_to(new_status)
if new_status == OrderStatus.CONFIRMED:
self._assign_driver(order)
if new_status == OrderStatus.DELIVERED and order.driver_id:
driver = self._drivers.get(order.driver_id)
if driver:
driver.status = DriverStatus.AVAILABLE
self._notify(order)
return order
def _assign_driver(self, order: Order) -> None:
"""Assign the nearest available driver."""
restaurant = self._restaurants[order.restaurant_id]
best_driver, best_dist = None, float('inf')
for driver in self._drivers.values():
if driver.status != DriverStatus.AVAILABLE:
continue
dist = self._distance(driver.lat, driver.lng, 0.0, 0.0) # simplified
if dist float:
return math.sqrt((lat2-lat1)**2 + (lng2-lng1)**2)
# ── Ratings ────────────────────────────────────────────────────────────────
def rate_order(self, order_id: str, restaurant_rating: float,
driver_rating: float) -> None:
order = self._get_order(order_id)
if order.status != OrderStatus.DELIVERED:
raise ValueError("Can only rate delivered orders")
restaurant = self._restaurants.get(order.restaurant_id)
if restaurant:
restaurant.update_rating(restaurant_rating)
driver = self._drivers.get(order.driver_id)
if driver:
driver.update_rating(driver_rating)
def _get_order(self, order_id: str) -> Order:
order = self._orders.get(order_id)
if not order:
raise ValueError(f"Order {order_id} not found")
return order
Usage Example
svc = DeliveryService()
# Setup restaurant
pizza_place = svc.register_restaurant("Tony's Pizza", "123 Main St", "Italian")
pizza_place.add_item(MenuItem("P1", "Margherita Pizza", 12.99, "Classic", "Pizza"))
pizza_place.add_item(MenuItem("P2", "Pepperoni Pizza", 14.99, "Spicy", "Pizza"))
pizza_place.add_item(MenuItem("D1", "Garlic Bread", 4.99, "Crispy", "Sides"))
# Customer builds cart and orders
svc.add_to_cart("cust_001", pizza_place.restaurant_id, "P1", quantity=2)
svc.add_to_cart("cust_001", pizza_place.restaurant_id, "D1", quantity=1)
order = svc.place_order("cust_001")
# [SMS] Customer cust_001: (placed notification)
# Restaurant confirms and prepares
svc.update_order_status(order.order_id, OrderStatus.CONFIRMED)
svc.update_order_status(order.order_id, OrderStatus.PREPARING)
svc.update_order_status(order.order_id, OrderStatus.READY)
# [PUSH] Driver: Order ready for pickup
svc.update_order_status(order.order_id, OrderStatus.PICKED_UP)
svc.update_order_status(order.order_id, OrderStatus.DELIVERED)
# [SMS] Customer cust_001: Your order has been delivered!
svc.rate_order(order.order_id, restaurant_rating=4.5, driver_rating=5.0)
print(f"Tony's Pizza rating: {pizza_place.rating:.1f}")
Design Patterns Applied
| Pattern | Where | Benefit |
|---|---|---|
| Observer | CustomerNotifier, DriverNotifier | Decouple order state changes from notification channels |
| State Machine | Order.transition_to() | Explicit valid transitions; invalid ones raise errors |
| Strategy | Driver assignment | Swap nearest-driver for zone-based or score-based assignment |
| Aggregate | Cart, Order | Cart aggregates CartItems; Order captures the confirmed snapshot |
🏢 Asked at: DoorDash Interview Guide
🏢 Asked at: Uber Interview Guide 2026: Dispatch Systems, Geospatial Algorithms, and Marketplace Engineering
🏢 Asked at: Airbnb Interview Guide 2026: Search Systems, Trust and Safety, and Full-Stack Engineering
🏢 Asked at: Shopify Interview Guide
🏢 Asked at: Lyft Interview Guide 2026: Rideshare Engineering, Real-Time Dispatch, and Safety Systems