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 |
{“@context”:”https://schema.org”,”@type”:”FAQPage”,”mainEntity”:[{“@type”:”Question”,”name”:”How do you model the order lifecycle in a food delivery system?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Use an OrderStatus enum with states: PLACED, CONFIRMED, PREPARING, READY_FOR_PICKUP, PICKED_UP, DELIVERED, CANCELLED. The Order class has a transition_to() method that validates allowed transitions before updating status: PLACED can go to CONFIRMED or CANCELLED; CONFIRMED to PREPARING or CANCELLED; PREPARING to READY_FOR_PICKUP; READY_FOR_PICKUP to PICKED_UP; PICKED_UP to DELIVERED. Invalid transitions raise ValueError. This explicit state machine prevents orders from jumping states (e.g., PLACED directly to DELIVERED) and makes the business rules auditable. Each transition also notifies registered observers (customer notifications, driver app updates) via the Observer pattern — the Order class calls notify_observers(new_status) on every valid transition.”}},{“@type”:”Question”,”name”:”How do you implement the Observer pattern for order status notifications?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Define an OrderObserver abstract base class with an on_status_change(order, status) method. Order stores a list of observers and calls each on_status_change() when status changes. Concrete observers: CustomerNotifier (sends push notification to customer: “Your order is being prepared”), DriverNotifier (alerts available drivers when order is READY_FOR_PICKUP). Register observers at order creation time: order.add_observer(CustomerNotifier(customer)); order.add_observer(DriverNotifier(driver_service)). Benefits: Order class has no knowledge of notification logic; new notification channels (SMS, email) are added by creating a new observer class without touching Order. This is the standard OOP interview answer for “how do components stay in sync without tight coupling.””}},{“@type”:”Question”,”name”:”How does driver assignment work in a food delivery system?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”The DeliveryService maintains a pool of available Driver objects, each with location (lat, lng) and status (AVAILABLE, ON_DELIVERY). assign_driver(order) finds the nearest available driver: compute Euclidean distance (or Haversine for geographic accuracy) from each available driver to the restaurant location, pick the minimum. Set driver.status = ON_DELIVERY, link driver to order, transition order to CONFIRMED. At scale, replace the linear scan with a geospatial index: store driver locations in Redis using GEOADD, query with GEODIST/GEORADIUS to find drivers within N km of the restaurant. Atomic assignment (preventing two orders from claiming the same driver) uses Redis SETNX on a per-driver lock key with a short TTL — if SETNX fails, try the next nearest driver.”}}]}
🏢 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