Back to LLD Problems: Parking Lot, Elevator, Chess, etc.
Q3Question 3 of 12

Design a Food Delivery App (like Swiggy/Zomato)

Key Concepts
  • An Order references a User, a Restaurant, and a list of MenuItem lines with quantities, and carries an OrderStatus.
  • The status progresses through PLACED, ACCEPTED, PREPARING, OUT_FOR_DELIVERY, and DELIVERED, with CANCELLED as an early exit.
  • Status transitions are strictly enforced so an order cannot jump illegally, for example from PLACED straight to DELIVERED.
  • Payment goes through a PaymentStrategy so the app supports card, UPI, and wallet interchangeably.
  • Observers react to status changes: user notifications, restaurant dashboard updates, delivery map refreshes, and loyalty points.
  • A DeliveryAgent is assigned when the food is ready, chosen by proximity to the restaurant.
  • Decoupling state transitions from side effects through the Observer pattern keeps the platform maintainable as features multiply.

Definition

A food delivery app runs an order through a strict status state machine from placed to delivered, with payment and agent assignment.
  • User – the customer who places orders and rates restaurants.
  • Restaurant – owns a Menu of MenuItem objects with prices.
  • Order – references a User, a Restaurant, and item lines with quantities; carries an OrderStatus.
  • OrderStatus – a state machine: PLACED → ACCEPTED → PREPARING → OUT_FOR_DELIVERY → DELIVERED, with CANCELLED as an early exit.
  • DeliveryAgent – assigned when food is ready, chosen by proximity.
  • PaymentStrategy – card, UPI, or wallet, interchangeable behind a common interface.

Think of placing a food order like initiating a payment that passes through several approval stages at National Bank of India. Rajesh picks items (fills the form), pays, the restaurant accepts (first approval), prepares (processing), a delivery agent picks it up (in transit), and it arrives (settled). At each stage the order can only move forward along allowed transitions, exactly like a transaction's lifecycle.

Understanding the Concepts

What are the core entities?

The core entities are User, Restaurant, MenuItem, Order, OrderItem, DeliveryAgent, OrderStatus, and PaymentStrategy.

JAVA
package com.bank.delivery.model;

import java.math.BigDecimal;

public class MenuItem {
    private final String itemId;
    private final String name;
    private final BigDecimal price;

    public MenuItem(String itemId, String name, BigDecimal price) {
        this.itemId = itemId;
        this.name = name;
        this.price = price;
    }

    public String getName() { return name; }
    public BigDecimal getPrice() { return price; }
}
JAVA
package com.bank.delivery.model;

public enum OrderStatus {
    PLACED, ACCEPTED, PREPARING, OUT_FOR_DELIVERY, DELIVERED, CANCELLED
}

How does the order status state machine work?

The order status progresses through a strict sequence. Legal transitions are enforced so an order cannot jump illegally.

JAVA
package com.bank.delivery.model;

import java.math.BigDecimal;
import java.util.List;
import java.util.ArrayList;

public class Order {
    private final String orderId;
    private final User user;
    private final Restaurant restaurant;
    private final List<OrderItem> items;
    private OrderStatus status;
    private DeliveryAgent agent;
    private final List<OrderStatusListener> listeners;

    public Order(String orderId, User user, Restaurant restaurant,
                 List<OrderItem> items) {
        this.orderId = orderId;
        this.user = user;
        this.restaurant = restaurant;
        this.items = items;
        this.status = OrderStatus.PLACED;
        this.listeners = new ArrayList<>();
    }

    public void transitionTo(OrderStatus newStatus) {
        if (!isValidTransition(status, newStatus)) {
            throw new IllegalStateException(
                    "Cannot transition from " + status
                    + " to " + newStatus);
        }
        this.status = newStatus;
        notifyListeners();
        System.out.println("Order " + orderId + " → " + newStatus);
    }

    private boolean isValidTransition(OrderStatus from,
                                       OrderStatus to) {
        return switch (from) {
            case PLACED -> to == OrderStatus.ACCEPTED
                    || to == OrderStatus.CANCELLED;
            case ACCEPTED -> to == OrderStatus.PREPARING
                    || to == OrderStatus.CANCELLED;
            case PREPARING -> to == OrderStatus.OUT_FOR_DELIVERY;
            case OUT_FOR_DELIVERY -> to == OrderStatus.DELIVERED;
            default -> false;
        };
    }

    public BigDecimal getTotal() {
        return items.stream()
                .map(i -> i.getMenuItem().getPrice()
                        .multiply(BigDecimal.valueOf(i.getQuantity())))
                .reduce(BigDecimal.ZERO, BigDecimal::add);
    }

    public void addListener(OrderStatusListener listener) {
        listeners.add(listener);
    }

    private void notifyListeners() {
        for (OrderStatusListener l : listeners) {
            l.onStatusChange(this, status);
        }
    }

    public String getOrderId() { return orderId; }
    public OrderStatus getStatus() { return status; }
    public void setAgent(DeliveryAgent agent) { this.agent = agent; }
}

How does the Observer pattern decouple reactions?

When the order status changes, many systems must react: user notification, restaurant dashboard, delivery map, loyalty points. Instead of the order calling each directly, it publishes a status-change event and every observer reacts independently.

JAVA
package com.bank.delivery.model;

public interface OrderStatusListener {
    void onStatusChange(Order order, OrderStatus newStatus);
}
JAVA
package com.bank.delivery.listener;

import com.bank.delivery.model.*;

public class UserNotificationListener implements OrderStatusListener {
    @Override
    public void onStatusChange(Order order, OrderStatus newStatus) {
        System.out.println("Push notification: Order "
                + order.getOrderId() + " is now " + newStatus);
    }
}
JAVA
package com.bank.delivery.listener;

import com.bank.delivery.model.*;

public class LoyaltyPointsListener implements OrderStatusListener {
    @Override
    public void onStatusChange(Order order, OrderStatus newStatus) {
        if (newStatus == OrderStatus.DELIVERED) {
            System.out.println("Credited loyalty points for order "
                    + order.getOrderId());
        }
    }
}

Adding a new reaction is a new listener class, not a change to the order code.

How does payment work?

Payment goes through a strategy interface so the app supports card, UPI, and wallet interchangeably.

JAVA
package com.bank.delivery.service;

import java.math.BigDecimal;

public interface PaymentStrategy {
    boolean pay(String userId, BigDecimal amount);
}

public class UpiPaymentStrategy implements PaymentStrategy {
    @Override
    public boolean pay(String userId, BigDecimal amount) {
        System.out.println("UPI payment of " + amount
                + " for user " + userId);
        return true;
    }
}

How is a delivery agent assigned?

When the food is ready (status PREPARING → OUT_FOR_DELIVERY), a delivery agent is assigned by proximity to the restaurant.

JAVA
package com.bank.delivery.service;

import com.bank.delivery.model.DeliveryAgent;
import java.util.List;
import java.util.Comparator;

public class AgentAssigner {
    public DeliveryAgent assignNearest(List<DeliveryAgent> available,
                                        double restaurantLat,
                                        double restaurantLon) {
        return available.stream()
                .min(Comparator.comparingDouble(
                        a -> distance(a.getLat(), a.getLon(),
                                restaurantLat, restaurantLon)))
                .orElseThrow(() -> new IllegalStateException(
                        "No delivery agent available"));
    }

    private double distance(double lat1, double lon1,
                            double lat2, double lon2) {
        return Math.sqrt(Math.pow(lat2 - lat1, 2)
                + Math.pow(lon2 - lon1, 2));
    }
}

How does rating work?

After delivery, ratings attach to both the restaurant and the agent.

JAVA
package com.bank.delivery.model;

public class Rating {
    private final String orderId;
    private final int restaurantScore; // 1-5
    private final int agentScore;      // 1-5
    private final String review;

    public Rating(String orderId, int restaurantScore,
                  int agentScore, String review) {
        this.orderId = orderId;
        this.restaurantScore = restaurantScore;
        this.agentScore = agentScore;
        this.review = review;
    }
}

What about extensibility?

The Observer pattern makes the design scale cleanly. When the product team wants to add a new reaction to order delivery, such as sending a feedback email, it is a new OrderStatusListener class with zero change to the order code.

What should a senior engineer know?

  • Status transitions and notifications are decoupled through Observer, which is what keeps a delivery platform maintainable as features multiply.
  • The payment strategy can also handle refunds on cancellation by adding a refund() method to the interface.
  • In production, agent assignment uses geospatial indexing (e.g., Redis GEO) rather than a brute-force distance scan.
  • Order status changes should be persisted in an event store for audit and replay.

What are common interview follow-up questions?

  • How would you add a promotion or discount feature?
  • How do you handle a delivery agent who becomes unavailable mid-delivery?
  • How do you prevent a restaurant from being overwhelmed with orders?
  • How would you implement real-time order tracking?
  • How does the system handle payment failure after the order is placed?

Visual Overview

DIAGRAM
  Order Status State Machine:
  ───────────────────────────

  PLACED ──accept──→ ACCEPTED ──prepare──→ PREPARING
    │                                          │
    │ cancel                            assign agent
    ▼                                          ▼
  CANCELLED               OUT_FOR_DELIVERY ──deliver──→ DELIVERED
DIAGRAM
  Observer Reactions on Status Change:
  ────────────────────────────────────

  Order.transitionTo(DELIVERED)
       │
       ├──→ UserNotificationListener  (push notification)
       ├──→ RestaurantDashboard       (update UI)
       ├──→ DeliveryMapListener       (clear tracking)
       └──→ LoyaltyPointsListener    (credit points)
DIAGRAM
  ┌──────────────────────────────────────┐
  │           OrderService                │
  │  place(user, restaurant, items)       │
  │  accept(order)                        │
  │  assignAgent(order)                   │
  └──────┬──────────┬──────┬─────────────┘
         │          │      │
         ▼          ▼      ▼
      Order     Payment  AgentAssigner
      Status    Strategy  (proximity)

Key Takeaway

When you design a food delivery app, the backbone is the order status state machine. An order progresses through placed, accepted, preparing, out for delivery, and delivered, with cancelled as an early exit. Enforcing legal transitions so an order can never jump illegally makes the whole system trustworthy.

The entities are a User who places orders, a Restaurant with a menu of items, an Order that references the user, the restaurant, and item lines, and a DeliveryAgent assigned when the food is ready, chosen by proximity. Payment goes through a payment strategy so the app supports card, UPI, and wallet interchangeably, and a successful payment is what moves the order into the placed state.

The pattern that makes the design scale cleanly is Observer. When the order status changes, many systems must react: the user gets a notification, the restaurant dashboard updates, the delivery map refreshes, and loyalty points accrue. Instead of the order calling each of these directly, it publishes a status-change event and every observer reacts independently. Adding a new reaction is a new listener rather than a change to the order code.

After delivery, ratings and reviews attach to both the restaurant and the agent, feeding future ranking. The senior insight is that decoupling state transitions from their side effects through Observer is what keeps a delivery platform maintainable as features multiply. At National Bank of India, payment lifecycles are modelled the same way, with a guarded state machine for the transaction and independent observers for notifications, ledger updates, and fraud checks.

Remember one sentence: a food delivery app is a strict order-status state machine with Observer-decoupled reactions and a pluggable payment strategy.

---

Share
Use ← → arrow keys to navigate