← Home
🏗Crack System Design with DSA AnimatorSystem DesignNEW
🏗 System Design›LLD Interview Prep›LLD
🍔

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.

📊 Medium⏱ 40–50 min🏢 Swiggy · Zomato · Uber Eats · DoorDash · Amazon · Flipkart · Grab
🎯 Interview goal

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.

💡
Clarify single city vs multi-city, payment timing, and cancellation rules before coding. Most loops want the happy path + cancel in one JVM.
❓

Clarifying questions (ask before coding)

Spend 3–5 minutes here — state transitions depend on business rules.

1Who can cancel — user, restaurant, or both? Until which state?
Why askDrives CANCELLED transitions and refund hooks.
2How is delivery agent assigned — nearest, round-robin, or manual?
Why askNatural fit for Strategy on OrderService.
3Payment — pay on order or on delivery?
Why askAffects when CONFIRMED happens relative to restaurant accept.
4Real-time tracking required?
Why askMention Observer or location updates — optional extension.
✅

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: Restaurant owns menu; OrderService orchestrates; DeliveryAgent pool 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.
ScopeIn scope (v1)Extension (mention)
DispatchNearest idle agent (Strategy)ETA prediction, batching
PaymentStub pay on confirmWallet, COD, refunds
ScaleOne city in-memoryShard 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.

  1. Enums: OrderStatus
  2. Domain: MenuItem, Restaurant, Order, DeliveryAgent
  3. Services: OrderService, DispatchStrategy
PLACEDCONFIRMEDPREPARINGREADY
ASSIGNEDPICKED_UPDELIVEREDCANCELLED

🟢 terminal / handoff 🔴 in-flight states — happy path left → right, top row then bottom

Food delivery order flow
User → restaurant → kitchen → agent → delivered
📐

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.”*

UML class diagram for food delivery
Main classes — Order, Restaurant, OrderService, DeliveryAgent

Relationships to label aloud

  • Association: Order → Restaurant + List<MenuItem>.
  • Association: Order → optional DeliveryAgent after assignment.
  • Dependency: OrderService uses DispatchStrategy to pick agent.
  • Facade: OrderService hides restaurant and agent registries.
🎨

Design patterns used (and why)

PatternWhereWhy (interview soundbite)
🪞FacadeOrderServiceOne API for user, restaurant, and agent callbacks.
🎬StateOrder + OrderStatusReject illegal jumps (deliver before pickup).
🎯StrategyDispatchStrategySwap nearest-agent vs load-balanced without changing service.
👁️Observer (ext)status listenersPush notifications / live tracking on state change.
💻

Full solution — enums & domain model

1. Order status & menu typesjava
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; }
}
2. Restaurantjava
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

3. Order with guarded transitionsjava
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; }
}
4. DeliveryAgentjava
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; }
}
5. OrderService facade + demojava
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 Order status + agent GPS stream.
  • Multi-restaurant cart: split into child orders — composite pattern.
  • Surge pricing: Strategy on fare calculation at placeOrder.
  • Payment + refund: hook on CONFIRMED and CANCELLED.
⚠️

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 OrderService with 400 lines and no Order domain logic.
  • Forgetting cancel and no agent available paths.
🧾

60-second cheat sheet

Ask firstCancel rules, payment timing, dispatch policy
Core classesOrder, Restaurant, DeliveryAgent, OrderService
StatesPLACED → … → DELIVERED; CANCELLED early
PatternsFacade, State, Strategy (dispatch)
APIplaceOrder → accept → ready → assign → deliver
🔗

More LLD problems