Skip to content
System Design

Learn

Design a Food Delivery App (LLD)

Low level design of a food delivery app: the domain model, the order state machine, cart and pricing with the decorator pattern, delivery partner assignment, and payment integration.

3 min readUpdated 2 Sept 2026

#LLD#State Machine#OOP#Machine Coding

A three-sided marketplace — customer, restaurant, delivery partner — which is what makes it a good LLD question: the order state machine has to coordinate three independent actors, and pricing has to compose many rules without turning into a wall of conditionals.

Requirements

  • Search restaurants by location, cuisine, rating; browse menus.
  • Cart, pricing with taxes, delivery fee and coupons; place an order and pay.
  • Restaurant accepts and prepares; a delivery partner is assigned and delivers.
  • Live tracking and status notifications; ratings afterwards.

The domain model

Customer          id, addresses, payment methods, order history
Restaurant        id, location, cuisines, menu, rating, open/closed, prep time
MenuItem          id, name, price, availability, customisations
Cart              customer, restaurant, items, applied coupon
Order             id, customer, restaurant, items, status, pricing, timestamps
DeliveryPartner   id, location, vehicle, status, current order
Payment           order, method, status, transactions

One constraint to state early: a cart belongs to exactly one restaurant. Adding an item from a second restaurant either clears the cart or is rejected — a rule that has to live in Cart, and one interviewers look for.

The order state machine

CREATED → PAID → CONFIRMED → PREPARING → READY_FOR_PICKUP
                                              ↓
                                       PARTNER_ASSIGNED
                                              ↓
                                        PICKED_UP → DELIVERED
     │         │           │
     └─────────┴───────────┴──→ CANCELLED  (rules differ per state)

Model transitions explicitly rather than with scattered conditionals:

java
enum OrderStatus {
    CREATED   { Set<OrderStatus> next() { return Set.of(PAID, CANCELLED); } },
    PAID      { Set<OrderStatus> next() { return Set.of(CONFIRMED, CANCELLED); } },
    CONFIRMED { Set<OrderStatus> next() { return Set.of(PREPARING, CANCELLED); } },
    PREPARING { Set<OrderStatus> next() { return Set.of(READY_FOR_PICKUP); } },
    // …
    ;
    abstract Set<OrderStatus> next();
}

The interesting rules are around cancellation: free before the restaurant confirms, partially charged once cooking starts, and not permitted after pickup. Encoding that as a policy per state — rather than an if chain in a service — is the design point.

Each transition emits an event that drives notifications to the relevant actors.

Pricing

Every real app accumulates rules — item total, packaging, delivery fee, surge, taxes, coupons, membership discounts, tips — and the naive implementation becomes one enormous method. Compose instead:

java
interface PricingRule {
    Money apply(Money runningTotal, OrderContext ctx);
}

List<PricingRule> pipeline = List.of(
    new ItemTotalRule(),
    new PackagingChargeRule(),
    new DeliveryFeeRule(),        // distance-based, waived above a threshold
    new SurgeRule(),              // weather, peak hours
    new CouponRule(coupon),       // applied BEFORE tax
    new TaxRule(),
    new TipRule(tip)
);

Money total = Money.zero(INR);
for (PricingRule rule : pipeline) total = rule.apply(total, ctx);

Order matters and is a business decision — a coupon applied after tax gives a different

total than before it. Making the pipeline an explicit ordered list turns that from a hidden bug into a visible configuration. Adding a rule means adding a class, not editing a method.

Use integer minor units for money throughout, as in payment gateway.

Delivery partner assignment

java
interface AssignmentStrategy {
    Optional<DeliveryPartner> assign(Order order, List<DeliveryPartner> candidates);
}

class NearestAvailableStrategy implements AssignmentStrategy { … }
class BatchedStrategy         implements AssignmentStrategy { … }  // multiple orders, one trip

Candidates come from a geospatial query around the restaurant, as in Uber. The timing subtlety worth raising: assign a partner

shortly before the food is ready, not at order time — assigning too early wastes the

partner's time, too late and the food goes cold. Predicted prep time drives it.

As with ride-hailing, a partner must not be assigned two orders at once unless batching is deliberate: an atomic compare-and-set on the partner's status is the mechanism.

Patterns worth naming

  • State — the order lifecycle; the pattern this problem is built around.
  • Strategy — assignment, pricing rules, search ranking.
  • Decorator or chain — the pricing pipeline.
  • Observer — status changes notifying customer, restaurant and partner.
  • Builder — constructing an order from a cart.
  • Repository — persistence abstraction, which keeps the domain testable.

Edge cases to handle

  • Restaurant closes or an item sells out after the order is placed → cancel and refund.
  • No delivery partner available → wait, widen the radius, then cancel with a refund.
  • Payment succeeds but order creation fails → refund via a compensating transaction.
  • Customer unreachable at delivery → a defined wait-and-return policy.
  • Coupon applied to an order later cancelled → the coupon must become reusable.
  • Concurrent edits to the same cart from two devices.

Extensions to be ready for

  • Scheduled orders — a future delivery slot, with prep timed backwards from it.
  • Group orders — several customers on one cart, split payment.
  • Live tracking — WebSocket location streaming, as in Uber.
  • Ratings — for restaurant, food and partner separately.
  • Multi-restaurant orders — several sub-orders under one parent, each with its own state.

Track this problem on the System Design sheet.

Frequently asked

How do you model the order lifecycle in a food delivery app?

As an explicit state machine where each status declares the states it may transition to, so invalid transitions are rejected in the domain rather than by scattered conditionals. Cancellation rules differ per state — free before the restaurant confirms, partially charged once cooking starts, disallowed after pickup — and encoding that as a per-state policy is what the question is testing.

How do you handle complex pricing without a giant method?

As an ordered pipeline of small pricing rules, each transforming a running total: item total, packaging, delivery fee, surge, coupon, tax, tip. Order is a business decision — a coupon before tax gives a different result than after — and making the sequence an explicit list turns that into visible configuration. Adding a rule then means adding a class rather than editing a method.

When should a delivery partner be assigned to an order?

Shortly before the food is ready, based on the restaurant's predicted preparation time — not at order placement. Assigning too early wastes the partner's time waiting; assigning too late means the food sits and goes cold. As in ride-hailing, the assignment itself must be an atomic state transition so one partner cannot be given two orders simultaneously.

Related