Design Food Delivery
Swiggy/Zomato-style order lifecycle — restaurant acceptance, prep, agent assignment, and delivery tracking. A staple at Uber Eats, DoorDash, Flipkart, and Amazon LLD loops.
Model Order with explicit state transitions, Restaurant menu & prep, OrderService facade, and DeliveryAgent assignment. Use State (or guarded enum) for illegal jumps; Strategy for nearest-agent dispatch.
The interview question
“Design a food delivery system like Swiggy/Zomato.” User places order at a restaurant; restaurant accepts and prepares; delivery agent picks up and delivers.
Interviewers care about order state machine, agent assignment, and a clean facade — not building maps routing from scratch.
Clarifying questions (ask before coding)
Spend 3–5 minutes here — state transitions depend on business rules.
OrderService.CONFIRMED happens relative to restaurant accept.Requirements & assumptions (state aloud)
- Functional: browse restaurant menu; place order; restaurant accept/reject; mark preparing → ready; assign agent; pickup → deliver.
- Order lifecycle:
PLACED → CONFIRMED → PREPARING → READY → ASSIGNED → PICKED_UP → DELIVERED; cancel from early states. - Structure:
Restaurantowns menu;OrderServiceorchestrates;DeliveryAgentpool for assignment. - v1: in-memory, single process; no geo-routing — distance stub or Manhattan metric.
- Out of scope: surge pricing, multi-restaurant cart, chat — mention as extensions.
| Scope | In scope (v1) | Extension (mention) |
|---|---|---|
| Dispatch | Nearest idle agent (Strategy) | ETA prediction, batching |
| Payment | Stub pay on confirm | Wallet, COD, refunds |
| Scale | One city in-memory | Shard by city, event bus |
High-level design & class relationships
OrderService is the facade — placeOrder, restaurantAccept, markReady, assignAgent, markDelivered.
State guards live on Order — illegal transitions return false instead of corrupting status.
- Enums:
OrderStatus - Domain:
MenuItem,Restaurant,Order,DeliveryAgent - Services:
OrderService,DispatchStrategy
🟢 terminal / handoff 🔴 in-flight states — happy path left → right, top row then bottom
UML class diagram (optional — if interviewer asks)
Sketch 6–8 classes if asked — focus on order state and service facade.
Say: *“Order owns status transitions; OrderService coordinates restaurant and agents.”*
Relationships to label aloud
- Association:
Order→Restaurant+List<MenuItem>. - Association:
Order→ optionalDeliveryAgentafter assignment. - Dependency:
OrderServiceusesDispatchStrategyto pick agent. - Facade:
OrderServicehides restaurant and agent registries.
Design patterns used (and why)
| Pattern | Where | Why (interview soundbite) |
|---|---|---|
| 🪞Facade | OrderService | One API for user, restaurant, and agent callbacks. |
| 🎬State | Order + OrderStatus | Reject illegal jumps (deliver before pickup). |
| 🎯Strategy | DispatchStrategy | Swap nearest-agent vs load-balanced without changing service. |
| 👁️Observer (ext) | status listeners | Push notifications / live tracking on state change. |
Full solution — enums & domain model
public enum OrderStatus {
PLACED, CONFIRMED, PREPARING, READY, ASSIGNED, PICKED_UP, DELIVERED, CANCELLED
}
public class MenuItem {
private final String itemId;
private final String name;
private final BigDecimal price;
// Creates an immutable menu item with id, name, and unit price
public MenuItem(String itemId, String name, BigDecimal price) {
this.itemId = itemId;
this.name = name;
this.price = price;
}
// Returns menu item id used in order line items
public String getItemId() { return itemId; }
// Returns display name for receipts and UI
public String getName() { return name; }
// Returns unit price for order total calculation
public BigDecimal getPrice() { return price; }
}public class Restaurant {
private final String restaurantId;
private final String name;
private final Map<String, MenuItem> menu = new LinkedHashMap<>();
private boolean acceptingOrders = true;
// Creates a restaurant with id and display name
public Restaurant(String restaurantId, String name) {
this.restaurantId = restaurantId;
this.name = name;
}
// Adds or replaces a menu item in this restaurant's catalog
public void addMenuItem(MenuItem item) {
menu.put(item.getItemId(), item);
}
// Returns read-only view of the menu for browsing and ordering
public Map<String, MenuItem> getMenu() {
return Collections.unmodifiableMap(menu);
}
// True when restaurant is open and accepting new orders
public boolean isAcceptingOrders() { return acceptingOrders; }
// Restaurant can pause intake during rush or closing hours
public void setAcceptingOrders(boolean accepting) {
this.acceptingOrders = accepting;
}
// Returns restaurant id for order routing and lookup
public String getRestaurantId() { return restaurantId; }
// Returns restaurant name for order confirmation screens
public String getName() { return name; }
}Full solution — Order, agent & OrderService
public class Order {
private final String orderId;
private final String userId;
private final Restaurant restaurant;
private final List<MenuItem> items;
private final BigDecimal total;
private OrderStatus status = OrderStatus.PLACED;
private DeliveryAgent assignedAgent;
private final Instant createdAt = Instant.now();
// Creates a new order in PLACED state with copied line items and computed total
public Order(String userId, Restaurant restaurant, List<MenuItem> items) {
this.orderId = UUID.randomUUID().toString();
this.userId = userId;
this.restaurant = restaurant;
this.items = List.copyOf(items);
this.total = items.stream()
.map(MenuItem::getPrice)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
// Restaurant accepts: PLACED → CONFIRMED; false if not PLACED
public synchronized boolean confirmByRestaurant() {
if (status != OrderStatus.PLACED) return false;
status = OrderStatus.CONFIRMED;
return true;
}
// Kitchen starts prep: CONFIRMED → PREPARING
public synchronized boolean startPreparing() {
if (status != OrderStatus.CONFIRMED) return false;
status = OrderStatus.PREPARING;
return true;
}
// Food ready for pickup: PREPARING → READY
public synchronized boolean markReady() {
if (status != OrderStatus.PREPARING) return false;
status = OrderStatus.READY;
return true;
}
// Links agent and moves READY → ASSIGNED
public synchronized boolean assignAgent(DeliveryAgent agent) {
if (status != OrderStatus.READY || agent == null || !agent.isAvailable()) return false;
this.assignedAgent = agent;
agent.markBusy(this);
status = OrderStatus.ASSIGNED;
return true;
}
// Agent collected order: ASSIGNED → PICKED_UP
public synchronized boolean markPickedUp() {
if (status != OrderStatus.ASSIGNED) return false;
status = OrderStatus.PICKED_UP;
return true;
}
// Delivery complete: PICKED_UP → DELIVERED; frees agent
public synchronized boolean markDelivered() {
if (status != OrderStatus.PICKED_UP) return false;
status = OrderStatus.DELIVERED;
if (assignedAgent != null) assignedAgent.markAvailable();
return true;
}
// Cancel from PLACED or CONFIRMED only; frees agent if already assigned
public synchronized boolean cancel() {
if (status != OrderStatus.PLACED && status != OrderStatus.CONFIRMED) return false;
status = OrderStatus.CANCELLED;
if (assignedAgent != null) assignedAgent.markAvailable();
return true;
}
// Returns current lifecycle state for UI and validation
public OrderStatus getStatus() { return status; }
// Returns unique order id for tracking and callbacks
public String getOrderId() { return orderId; }
// Returns customer id who placed the order
public String getUserId() { return userId; }
// Returns restaurant fulfilling this order
public Restaurant getRestaurant() { return restaurant; }
// Returns immutable line items for receipts
public List<MenuItem> getItems() { return items; }
// Returns computed order total at placement time
public BigDecimal getTotal() { return total; }
// Returns assigned agent after ASSIGNED state; null before
public DeliveryAgent getAssignedAgent() { return assignedAgent; }
}public class DeliveryAgent {
private final String agentId;
private final String name;
private double lat;
private double lng;
private boolean available = true;
private Order currentOrder;
// Creates an agent with id, name, and initial GPS coordinates
public DeliveryAgent(String agentId, String name, double lat, double lng) {
this.agentId = agentId;
this.name = name;
this.lat = lat;
this.lng = lng;
}
// True when agent has no active delivery assignment
public boolean isAvailable() { return available; }
// Marks agent busy and links active order after assignment
public void markBusy(Order order) {
this.available = false;
this.currentOrder = order;
}
// Marks agent idle after delivery or cancellation
public void markAvailable() {
this.available = true;
this.currentOrder = null;
}
// Updates live location for nearest-agent dispatch
public void updateLocation(double lat, double lng) {
this.lat = lat;
this.lng = lng;
}
// Returns agent id for assignment records
public String getAgentId() { return agentId; }
// Returns latitude for distance calculations
public double getLat() { return lat; }
// Returns longitude for distance calculations
public double getLng() { return lng; }
// Returns agent display name for customer tracking UI
public String getName() { return name; }
// Returns order being delivered; null when available
public Order getCurrentOrder() { return currentOrder; }
}public interface DispatchStrategy {
// Picks best idle agent for this order; empty if none available
Optional<DeliveryAgent> selectAgent(Order order, List<DeliveryAgent> agents);
}
public class NearestAgentStrategy implements DispatchStrategy {
// Chooses available agent with minimum Manhattan distance to restaurant stub (0,0)
@Override
public Optional<DeliveryAgent> selectAgent(Order order, List<DeliveryAgent> agents) {
return agents.stream()
.filter(DeliveryAgent::isAvailable)
.min(Comparator.comparingDouble(a -> Math.abs(a.getLat()) + Math.abs(a.getLng())));
}
}
public class OrderService {
private final Map<String, Restaurant> restaurants = new ConcurrentHashMap<>();
private final Map<String, Order> orders = new ConcurrentHashMap<>();
private final List<DeliveryAgent> agents = new CopyOnWriteArrayList<>();
private final DispatchStrategy dispatchStrategy;
// Wires order service with injectable agent dispatch algorithm
public OrderService(DispatchStrategy dispatchStrategy) {
this.dispatchStrategy = dispatchStrategy;
}
// Registers a restaurant available for ordering
public void registerRestaurant(Restaurant r) {
restaurants.put(r.getRestaurantId(), r);
}
// Registers a delivery agent in the pool
public void registerAgent(DeliveryAgent agent) {
agents.add(agent);
}
// Validates menu items, creates order in PLACED; empty if restaurant closed or bad items
public Optional<Order> placeOrder(String userId, String restaurantId, List<String> itemIds) {
Restaurant r = restaurants.get(restaurantId);
if (r == null || !r.isAcceptingOrders()) return Optional.empty();
List<MenuItem> items = new ArrayList<>();
for (String id : itemIds) {
MenuItem m = r.getMenu().get(id);
if (m == null) return Optional.empty();
items.add(m);
}
Order order = new Order(userId, r, items);
orders.put(order.getOrderId(), order);
return Optional.of(order);
}
// Restaurant accepts order: PLACED → CONFIRMED
public boolean restaurantAccept(String orderId) {
Order o = orders.get(orderId);
return o != null && o.confirmByRestaurant();
}
// Restaurant marks food ready and triggers agent assignment
public boolean restaurantMarkReady(String orderId) {
Order o = orders.get(orderId);
if (o == null) return false;
if (!o.startPreparing()) return false;
if (!o.markReady()) return false;
return assignDeliveryAgent(orderId);
}
// Uses strategy to pick agent and transition READY → ASSIGNED
public boolean assignDeliveryAgent(String orderId) {
Order o = orders.get(orderId);
if (o == null) return false;
Optional<DeliveryAgent> pick = dispatchStrategy.selectAgent(o, agents);
return pick.isPresent() && o.assignAgent(pick.get());
}
// Agent confirms pickup: ASSIGNED → PICKED_UP
public boolean agentPickup(String orderId) {
Order o = orders.get(orderId);
return o != null && o.markPickedUp();
}
// Agent confirms delivery: PICKED_UP → DELIVERED
public boolean agentDeliver(String orderId) {
Order o = orders.get(orderId);
return o != null && o.markDelivered();
}
// User or restaurant cancels early-stage order
public boolean cancelOrder(String orderId) {
Order o = orders.get(orderId);
return o != null && o.cancel();
}
// Lookup order by id for status APIs
public Optional<Order> getOrder(String orderId) {
return Optional.ofNullable(orders.get(orderId));
}
}
public class FoodDeliveryDemo {
// End-to-end: place → accept → ready → assign → pickup → deliver
public static void main(String[] args) {
OrderService svc = new OrderService(new NearestAgentStrategy());
Restaurant r = new Restaurant("R1", "Burger Hub");
r.addMenuItem(new MenuItem("B1", "Classic Burger", new BigDecimal("199")));
svc.registerRestaurant(r);
svc.registerAgent(new DeliveryAgent("A1", "Raj", 1.0, 2.0));
svc.placeOrder("user1", "R1", List.of("B1")).ifPresent(o -> {
svc.restaurantAccept(o.getOrderId());
svc.restaurantMarkReady(o.getOrderId());
svc.agentPickup(o.getOrderId());
svc.agentDeliver(o.getOrderId());
svc.getOrder(o.getOrderId())
.ifPresent(done -> System.out.println("Status: " + done.getStatus()));
});
}
}While you code — questions interviewers ask
Extensions & follow-ups
- Live tracking: Observer on
Orderstatus + agent GPS stream. - Multi-restaurant cart: split into child orders — composite pattern.
- Surge pricing: Strategy on fare calculation at
placeOrder. - Payment + refund: hook on
CONFIRMEDandCANCELLED.
Common mistakes (fail signals)
- String status with no transition guards — illegal states slip through.
- Assigning agent before food is READY.
- No rollback when assignment fails mid-batch.
- God class
OrderServicewith 400 lines and noOrderdomain logic. - Forgetting cancel and no agent available paths.