Definition
- User – the customer who places orders and rates restaurants.
- Restaurant – owns a
MenuofMenuItemobjects 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.
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; }
}
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.
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.
package com.bank.delivery.model;
public interface OrderStatusListener {
void onStatusChange(Order order, OrderStatus newStatus);
}
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);
}
}
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.
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.
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.
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
Order Status State Machine:
───────────────────────────
PLACED ──accept──→ ACCEPTED ──prepare──→ PREPARING
│ │
│ cancel assign agent
▼ ▼
CANCELLED OUT_FOR_DELIVERY ──deliver──→ DELIVERED
Observer Reactions on Status Change:
────────────────────────────────────
Order.transitionTo(DELIVERED)
│
├──→ UserNotificationListener (push notification)
├──→ RestaurantDashboard (update UI)
├──→ DeliveryMapListener (clear tracking)
└──→ LoyaltyPointsListener (credit points)
┌──────────────────────────────────────┐
│ 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.
---