Design a Food Delivery System
Restaurants, menus, carts, orders, and delivery-partner matching — Swiggy/DoorDash object modelling.
Problem Statement
Design the object model for a food delivery system like Swiggy or Zomato. Customers browse restaurants, add available menu items to a cart, place an order, and then track the order as the restaurant accepts it, prepares it, hands it to a delivery partner, and the partner delivers it.
The interview focus is the order lifecycle and partner matching. A strong solution should make state transitions explicit, keep assignment policy pluggable, calculate pricing in one place, and notify customers, restaurants, and partners whenever the order status changes.
Business context
Food delivery is a high-frequency intermediate LLD problem because it combines marketplace modelling with a lifecycle that is easy to get wrong. Restaurants change menus, customers edit carts, partners move across the city, and the same order must not jump from Placed directly to Delivered.
Interviewers use it to test whether you can separate domain state from service orchestration: Order owns valid transitions, AssignmentStrategy owns partner choice, OrderService coordinates the use case, and OrderObserver fans out status notifications without coupling the lifecycle to SMS, push, or email.
Functional Requirements
Register restaurants with a location, availability flag, and menu items.
Allow customers to build a cart for exactly one restaurant at a time.
Validate item availability and quantity before adding items to the cart.
Place an order from a non-empty cart and compute subtotal, delivery fee, platform fee, tax, and total.
Move the order through Placed -> Accepted -> Preparing -> OutForDelivery -> Delivered, with Cancelled as a terminal failure state.
Let the restaurant accept an order and mark it as preparing before dispatch.
Assign a delivery partner using a pluggable nearest or least-busy strategy.
Notify customers, restaurants, and delivery partners whenever the order status changes.
Reject invalid lifecycle transitions such as dispatch before preparation or delivery before dispatch.
Non-Functional Requirements
Lifecycle correctness
The state machine must be the source of truth for valid transitions. Service methods should ask the order to transition rather than mutate a status enum directly.
Low assignment latency
For an interview-scale in-memory design, scanning partners is acceptable. The strategy seam lets production replace it with a geospatial index later.
Extensibility
New pricing plans, partner assignment policies, notification channels, and cancellation rules should be additive classes with minimal changes to the core flow.
Auditability
Every order should keep a status timeline so support teams can explain when the restaurant accepted, when dispatch happened, and why a cancellation was allowed.
Consistency under concurrency
Order transition and partner assignment should be performed atomically per order so two dispatch attempts do not assign two partners.
Requirement Clarification
QCan a cart contain items from multiple restaurants?
No for the base design. One cart maps to one restaurant, which keeps pricing, preparation, and partner pickup simple. Multi-restaurant checkout is an extension.
QWho can cancel an order and until when?
Assume cancellation is allowed before delivery and becomes a terminal Cancelled state. Real systems can add policy checks based on payment, preparation stage, or partner arrival.
QDo we model payments and refunds?
Only pricing is in scope. Payment authorization, refunds, coupon validation, and wallet balances are boundary integrations that can be added around OrderService.
QHow exact should location matching be?
Use a simple distance function in the LLD. The important design point is that nearest and least-busy are interchangeable AssignmentStrategy implementations.
QShould notifications be synchronous?
In the interview implementation observers run synchronously to show the pattern. In production they would usually publish events to a queue for retry and fanout.
UML Class Diagram
Sequence Diagram
Entity Identification
Restaurant
Owns restaurant identity, location, menu, and whether the kitchen is currently accepting orders.
MenuItem
Represents one sellable item with price and availability. The cart reads it, but availability remains controlled by the restaurant menu.
Cart
Temporary customer basket for one restaurant. It validates quantity and item availability, and exposes an immutable snapshot for order creation.
Order
Aggregate root for the order lifecycle. It owns items, pricing quote, partner, status timeline, and all valid transitions.
DeliveryPartner
Courier domain object with location, online status, capacity, and active order count used by assignment strategies.
AssignmentStrategy
Policy interface for partner matching. Nearest and least-busy are alternative implementations with the same service contract.
OrderService
Application facade. Registers restaurants and partners, places orders through the factory, invokes transitions, assigns partners, and notifies observers.
OrderObserver
Notification seam. Customer, restaurant, and partner channels react to order updates without being embedded inside the state machine.
Design Patterns Used
Order delegates transition behavior to status-specific state objects. Invalid jumps such as Placed to OutForDelivery fail at the domain boundary instead of relying on scattered conditionals.
AssignmentStrategy lets the service choose nearest or least-busy partner matching at runtime. The same pattern also keeps pricing rules behind PricingStrategy.
OrderObserver decouples lifecycle changes from notification delivery. Adding WhatsApp, push, email, or restaurant dashboard alerts does not change Order.
OrderFactory defines the order creation skeleton and delegates the actual object construction to newOrder, so premium, scheduled, or group-order factories can vary creation without changing OrderService.
Step-by-Step Design
1Start with the core marketplace nouns
Keep the base model small: Restaurant owns a menu and location, MenuItem owns price and availability, and Cart is tied to one restaurant so pickup and billing stay coherent.
public final class Cart { private final Restaurant restaurant; private final Map<String, Line> lines = new LinkedHashMap<>(); public void addItem(String itemId, int quantity) { MenuItem item = restaurant.menuItem(itemId); if (!item.isAvailable()) throw new IllegalStateException(); lines.put(itemId, new Line(item, quantity)); } }2Make order status a real state machine
Order should expose intent methods such as accept, markPreparing, and markDelivered. Each method delegates to the current state so invalid transitions are impossible to ignore.
public synchronized void markOutForDelivery() { state.dispatch(this); } private static final class PreparingState extends BaseState { public OrderStatus status() { return OrderStatus.PREPARING; } public void dispatch(Order order) { order.transitionTo(new OutForDeliveryState()); } }3Snapshot price at order creation
The order should carry a PricingQuote computed from the cart. Later menu price changes should not rewrite what the customer agreed to pay.
4Hide partner matching behind a strategy
The service should not know whether matching means closest by location, least busy, highest rating, or partner preference. It asks AssignmentStrategy for one available partner.
public interface AssignmentStrategy { DeliveryPartner assign(Order order, List<DeliveryPartner> partners); }5Notify through observers, not conditionals
After each successful transition, OrderService emits an update to registered observers. Channels can be added or removed without touching lifecycle code.
6Keep the service as orchestration only
OrderService validates registered restaurants, invokes the factory, calls transition methods, asks the assignment strategy, and notifies observers. It should not contain status-specific branching.
Complete Java Implementation
Explanation of Every Class
MenuItem
Immutable identity, name, and price with a synchronized availability flag. The cart reads availability before adding an item, while restaurants retain control over toggling items on or off.
Restaurant
Restaurant owns menu and location. The nested Location value object gives assignment strategies a simple distance function without introducing map-provider dependencies.
Cart
Cart binds a customer to one restaurant and stores item lines. It validates quantity and availability, computes subtotal, and returns snapshots so an order is insulated from later cart edits.
Order
Order is the lifecycle aggregate. It stores the pricing quote, selected items, optional partner, timeline, and nested state objects for Placed, Accepted, Preparing, OutForDelivery, Delivered, and Cancelled.
DeliveryPartner
Tracks partner identity, location, online flag, capacity, and active orders. Assignment strategies use distanceTo and activeOrderCount, while the order releases capacity on delivery or cancellation.
AssignmentStrategy
Strategy interface with two nested implementations. NearestPartnerStrategy minimizes pickup distance; LeastBusyPartnerStrategy minimizes active orders and uses distance as a tie-breaker.
OrderService
Application facade plus nested collaborators for observers, pricing, quote, and order factory. It orchestrates placement, state transitions, partner assignment, and notification fanout.
Main
Small executable demo wiring a restaurant, menu, partners, nearest strategy, default pricing, observers, and the complete happy path from placement to delivery.
Dry Run
Sample input
Restaurant Dosa Hub has Masala Dosa at 12000 paise and Filter Coffee at 5000 paise. Cart has 2 dosas and 1 coffee. Partners: Priya is nearest with 0 active orders, Aman is farther with 0 active orders.
| Step | Action | Order status | Partner state | Notification | Result |
|---|---|---|---|---|---|
| 1 | placeOrder(cart) | PLACED | No partner | Customer + restaurant see placed | Quote = 29000 + 3900 + 600 + 1450 = 34950 paise |
| 2 | acceptOrder(orderId) | ACCEPTED | No partner | Restaurant accepted | State machine allows next step to prepare |
| 3 | startPreparing(orderId) | PREPARING | No partner | Preparing update | Order is now eligible for dispatch |
| 4 | dispatchOrder(orderId) | OUT_FOR_DELIVERY | Priya active orders = 1 | Assigned to Priya | Nearest strategy chooses Priya |
| 5 | deliverOrder(orderId) | DELIVERED | Priya active orders = 0 | Delivered update | Terminal success state |
The trace shows why state and strategy are separate. The order decides whether dispatch is legal; the strategy decides which partner receives the dispatched order.
Complexity Analysis
| Operation | Time | Space | Note |
|---|---|---|---|
| add item to cart | O(1) | O(1) | Menu lookup and line merge are map operations for a single restaurant. |
| place order | O(I) | O(I) | I cart lines are copied into an immutable order snapshot and priced. |
| state transition | O(N) | O(1) | The transition is constant; notifying N observers dominates. |
| assign nearest partner | O(P) | O(1) | Scans P partners and keeps the smallest distance to the restaurant. |
| assign least-busy partner | O(P) | O(1) | Scans P partners, comparing active order count and then distance. |
For interview scale, O(P) partner scans are clear and acceptable. At city scale, replace the list with a geo-index plus availability buckets while preserving the AssignmentStrategy interface.
Extensibility
New assignment policy
Add a new AssignmentStrategy such as highest-rated, zone-aware, or partner-preferred. OrderService.dispatchOrder remains unchanged.
New pricing rule
Implement PricingStrategy for surge fee, coupon discount, restaurant packaging charge, or subscription free delivery. Orders still receive a quote snapshot.
New notification channel
Implement OrderObserver for push, SMS, WhatsApp, email, restaurant dashboard, or partner app. No lifecycle code changes are required.
Scheduled or group orders
Subclass OrderFactory and override newOrder to create scheduled or group-order variants while keeping validation and pricing in the factory skeleton.
Cancellation policy
Add a policy object consulted by Order.cancel or OrderService.cancelOrder to block cancellation after partner arrival or to compute a fee.
Alternative Designs
Enum status with switch statements
Store OrderStatus directly and use switch statements in OrderService to validate transitions.
Tradeoffs
Less code for a toy solution, but transition rules leak into orchestration and become harder to extend or test as cancellation, refund, and failure states grow.
Dedicated dispatch service
Move partner assignment into a separate DispatchService that owns partner pools, geospatial indexing, retries, and reassignment after partner rejection.
Tradeoffs
Better production separation, but for an LLD interview it can distract from the core order aggregate unless the interviewer asks about scale.
Event-sourced order lifecycle
Persist events such as OrderPlaced, RestaurantAccepted, PartnerAssigned, and OrderDelivered, then rebuild current state from the event stream.
Tradeoffs
Excellent auditability and replay, but more infrastructure and eventual consistency than needed for the base object model.
Common Mistakes
- ×
Treating order status as a public mutable enum and allowing any service method to set it directly.
- ×
Putting nearest and least-busy partner matching in one long if block inside OrderService.
- ×
Forgetting to snapshot menu prices at order creation, so later menu edits change historical orders.
- ×
Not releasing a partner's active order slot after delivery or cancellation.
- ×
Sending notifications from inside every state class instead of through a centralized observer fanout.
- ×
Allowing dispatch before the restaurant has accepted and started preparing the order.
- ×
Mixing payment, coupon, and refund workflows into the core lifecycle before clarifying scope.
Follow-up Interview Questions
QHow would you support partner rejection after assignment?
Add an Assigned state or dispatch attempt object. If a partner rejects, release that partner, record the attempt, and ask the assignment strategy for the next candidate.
QHow would you make nearest-partner lookup faster than O(P)?
Index online partners by geohash or grid cell, then query nearby cells first. The optimization sits behind AssignmentStrategy so the order flow is unchanged.
QHow do you prevent two dispatchers from assigning two partners?
Synchronize or transact on the order row during dispatch. The check that the order is Preparing and the write to OutForDelivery plus partner assignment must be atomic.
QWhere do refunds and cancellation fees fit?
Use a cancellation policy plus payment adapter around OrderService.cancelOrder. The order marks a legal terminal state; the payment workflow handles money movement.
QHow would you notify millions of customers reliably?
Make observers publish domain events to a durable queue. Workers perform push, SMS, email, and retries with idempotency keys based on order id and status.
Production Considerations
Persistence and transactions
Persist orders, menu snapshots, partner assignments, and status events. Dispatch should use a transaction or compare-and-set from Preparing to OutForDelivery.
Geospatial matching
Store partner locations in a geospatial index and update them through a streaming channel. Strategy implementations can query nearby partners instead of scanning a list.
Notification reliability
Replace synchronous observers with durable events, retries, dead-letter queues, and idempotent notification sends.
Menu and price consistency
Cache menus carefully, but snapshot item names, quantities, and prices into the order so support and refunds use the original customer agreement.
Observability
Track order transition latency, restaurant acceptance time, partner assignment success rate, cancellation reason, and notification delivery failures.
What Interviewers Look For
Does the candidate make the order lifecycle explicit instead of scattering status checks?
Can they explain why partner matching is a Strategy and when nearest versus least-busy should be used?
Do they separate pricing snapshot, payment, and notification concerns?
Do they preserve invariants around dispatch and terminal states?
Can they describe the production upgrade path without overbuilding the base LLD?
Quiz
0/5 answered
1.Why use the State pattern for **Order**?
2.What does **AssignmentStrategy** make easy to change?
3.Why should price be snapshotted when the order is created?
4.Which component should send status updates to customer, restaurant, and partner channels?
5.What must be atomic during dispatch?
Practice Variants
Partner rejection and reassignment
AdvancedAdd a PartnerAssigned state and allow a partner to reject. Re-run assignment while avoiding partners who already rejected the order.
Coupon and subscription pricing
IntermediateAdd pricing strategies for coupons, free-delivery subscription, and surge fee while keeping OrderFactory unchanged.
Multi-restaurant checkout
AdvancedSplit one checkout into multiple restaurant-specific orders under a parent checkout id. Decide how partner assignment and cancellation work per child order.
Flashcards
Cheat Sheet
Entities: Restaurant, MenuItem, Cart, Order, DeliveryPartner, AssignmentStrategy, OrderService, OrderObserver.
Lifecycle: Placed -> Accepted -> Preparing -> OutForDelivery -> Delivered. Cancelled is terminal for allowed failure paths.
Patterns: State for lifecycle, Strategy for assignment and pricing, Observer for notifications, Factory Method for order creation.
Pricing: snapshot subtotal, delivery fee, platform fee, tax, and total at order creation.
Partner matching: nearest minimizes pickup distance; least-busy minimizes active orders and uses distance as tie-breaker.
Invariants: one restaurant per cart; non-empty cart to place order; no dispatch before preparation; no delivery before dispatch; release partner capacity on terminal state.
Scale path: persist status events, use row locks or compare-and-set for dispatch, replace partner list scan with geospatial index, publish notifications through a queue.
References
- BookDesign Patterns: Elements of Reusable Object-Oriented Software — Gamma, Helm, Johnson, and Vlissides
- DocsRefactoring Guru — State Pattern
- BlogMartin Fowler — Domain Event — Martin Fowler