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, transactionsOne 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:
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:
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
interface AssignmentStrategy {
Optional<DeliveryPartner> assign(Order order, List<DeliveryPartner> candidates);
}
class NearestAvailableStrategy implements AssignmentStrategy { … }
class BatchedStrategy implements AssignmentStrategy { … } // multiple orders, one tripCandidates 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.