18. Restaurant / Food Delivery System Intermediate → Advanced

A. Realistic Interview Prompt

"Design a food delivery system like DoorDash or Swiggy. Customers order from a restaurant's menu, the order moves through preparation and delivery, and a delivery partner is assigned to carry it. Customers should see live status updates."

B. Difficulty

Intermediate, climbing to Advanced on the delivery-assignment and status-ordering follow-ups. The order lifecycle itself is a clean, bounded state machine; the real signal is whether the candidate snapshots menu-item price/name at order time instead of pointing at a live, mutable catalog record, and whether delivery-partner assignment is race-free.

C. What This Problem Tests

D. Requirements

Functional

  1. Browse a restaurant's menu and add items to a cart
  2. Place an order, transitioning it through placed → confirmed → preparing → out-for-delivery → delivered
  3. Assign an available delivery partner to an order ready for pickup
  4. Push real-time status updates to the customer as the order progresses
  5. Reflect a historical order's price exactly as charged, even if the menu item's price later changes

Non-Functional

E. Clarifying Questions

#QuestionWhy It MattersDesign Decision Affected
1How is a delivery partner chosen — nearest available, least currently loaded, or some combination?Determines whether assignment needs to be a Strategy or can be one fixed ruleDesign of DeliveryAssignmentStrategy
2Can status updates arrive out of order or be duplicated (e.g. a flaky delivery-partner app resending "picked up")?Confirms the order's State machine must reject illegal/duplicate transitions rather than trust the inputLegality checks inside OrderState
3If a menu item's price changes after an order was placed, should the historical order reflect the old or new price?Determines whether Order needs its own price snapshot or can reference the live MenuItemPresence of OrderLineItem as a frozen snapshot
4Can a customer cancel after the restaurant has started preparing the order?Affects which states permit a cancellation transitionScope of the OrderState transition table

F. Assumptions

G. Actors

ActorResponsibility
CustomerBrowses menu, places order, receives status updates
RestaurantConfirms orders, updates preparation status
Delivery PartnerPicks up and delivers the order, reports pickup/delivery events
OrderService (facade)Top-level API coordinating ordering, assignment, and status

H. Core Domain Entities

ClassResponsibilityKey AttributesKey MethodsDoes NOT Handle
MenuItem (catalog)A restaurant's live, editable menu entryname, price, available—Anything about a specific past order — delegates to OrderLineItem snapshots
OrderLineItem (snapshot)One ordered item frozen at checkout timenameAtOrderTime, priceAtOrderTime, quantity—Live catalog lookups
OrderOne order and its lifecycle statuscustomer, restaurant, lineItems, statustransitionTo(status), cancel()Delivery-partner selection logic itself
OrderState (interface)One lifecycle state's legal next transitions—onConfirm(), onPrepared(), onPickedUp(), onDelivered()—
DeliveryPartnerOne delivery courier and their current availabilityid, status, locationclaim()—
DeliveryAssignmentStrategy (interface)One partner-selection rule—selectPartner(order, candidates)—
DeliveryDispatcherAtomically selects and claims a partner—assign(order)Delivery-route logistics
OrderStatusNotifierObserver subject pushing status updates—notifyStatusChanged(order)—
OrderService (facade)Top-level orchestration—placeOrder(), updateStatus()—

I. Class Relationships

J. Class Diagram

1

*

MenuItem

name

price

available

OrderLineItem

nameAtOrderTime

priceAtOrderTime

quantity

Order

status

+transitionTo(status)

+cancel()

«interface»

OrderState

+onConfirm()

+onPrepared()

+onPickedUp()

+onDelivered()

DeliveryPartner

status

location

+claim()

«interface»

DeliveryAssignmentStrategy

+selectPartner(order, candidates)

DeliveryDispatcher

+assign(order)

OrderStatusNotifier

+notifyStatusChanged(order)

View Mermaid source
classDiagram
  class MenuItem {
    name
    price
    available
  }
  class OrderLineItem {
    nameAtOrderTime
    priceAtOrderTime
    quantity
  }
  class Order {
    status
    +transitionTo(status)
    +cancel()
  }
  class OrderState {
    <<interface>>
    +onConfirm()
    +onPrepared()
    +onPickedUp()
    +onDelivered()
  }
  class DeliveryPartner {
    status
    location
    +claim()
  }
  class DeliveryAssignmentStrategy {
    <<interface>>
    +selectPartner(order, candidates)
  }
  class DeliveryDispatcher {
    +assign(order)
  }
  class OrderStatusNotifier {
    +notifyStatusChanged(order)
  }
  MenuItem --> OrderLineItem
  Order "1" --> "*" OrderLineItem
  Order --> OrderState
  Order --> OrderStatusNotifier
  DeliveryDispatcher --> DeliveryAssignmentStrategy
  DeliveryDispatcher --> DeliveryPartner

K. Responsibility Mapping

ClassResponsibilityDoes NOT Handle
OrderLineItemFrozen price/name at order timeLive catalog state
OrderState implementationsLegal transitions from one specific statusNotification, delivery assignment
DeliveryDispatcherAtomic select-then-claim of a partnerRoute optimization, ETA calculation
OrderStatusNotifierFan-out of status-change eventsDeciding whether a transition is legal
OrderServiceTop-level orchestrationState-transition legality, partner selection math

L. SOLID Analysis

PrincipleHow It's AppliedNaive Violation
SRPOrderState owns transition legality; DeliveryAssignmentStrategy owns partner selection; OrderStatusNotifier owns fan-outOne Order class validating transitions, picking a partner, and pushing notifications all inline
OCPA new assignment rule is a new DeliveryAssignmentStrategy; a new lifecycle state is a new OrderStateA status-transition if/else chain edited every time a new intermediate status is added
LSPEvery OrderState honestly rejects transitions it doesn't support instead of silently accepting themA state that quietly allows "delivered" from "placed" to avoid throwing an error, masking a real bug
ISPDeliveryAssignmentStrategy stays a single-method interfaceA combined "DeliveryPolicy" interface mixing partner selection with pricing/tipping rules
DIPDeliveryDispatcher depends on the DeliveryAssignmentStrategy abstraction, not a hardcoded nearest-partner formulaOrder.assignPartner() embedding distance math directly on the entity

M. Design Patterns

PatternProblem It SolvesWhere It AppearsWhen NOT to Use
StateAn order's legal next transitions genuinely depend on its current statusOrderStateSkip if the round only needs a single "is this order done" boolean
StrategyDelivery-partner selection rules vary and change more often than dispatch mechanicsDeliveryAssignmentStrategySkip if partner assignment is manual/out of scope
ObserverPushing status updates shouldn't require the transition code to know who's watchingOrderStatusNotifierSkip if live status push is out of scope for the round

Explicitly avoided: a live reference from Order to MenuItem for pricing — see I for why this corrupts order history the moment a price changes.

N. What Is Likely to Change?

BehaviorCurrent DesignAnticipated ChangeProtecting Abstraction
Delivery assignment ruleNearest-available or load-balancedML-ranked assignment factoring in partner rating, traffic, batch-delivery groupingDeliveryAssignmentStrategy interface
Order lifecyclePlaced/confirmed/preparing/out-for-delivery/deliveredAdd a "batched with another order" intermediate stateNew OrderState implementation, additive
Notification channelsPush notificationSMS fallback if push failsReuses Notification System's (Problem 7) existing Chain of Responsibility fallback, not rebuilt here

O. Design Evolution

What stays stable: OrderService's public place/update API. New states and assignment rules are additive.

P. API / Interface Design

interface OrderState {
  OrderState onConfirm();
  OrderState onPrepared();
  OrderState onPickedUp();
  OrderState onDelivered();
}

interface DeliveryAssignmentStrategy {
  DeliveryPartner selectPartner(Order order, List<DeliveryPartner> candidates);
}

class OrderService {
  Order placeOrder(Customer customer, Restaurant restaurant, Cart cart);
  void updateStatus(String orderId, StatusEvent event);
}

Ownership/validation notes: updateStatus() delegates to the order's current OrderState, which either returns the next state or rejects the event outright — OrderService itself never decides transition legality, only routes the event.

Q. Pseudocode — Critical Logic

Order.transitionTo(event):
  nextState = this.state.handle(event)   // OrderState decides legality
  if nextState is REJECTED: reject
  this.state = nextState
  orderStatusNotifier.notifyStatusChanged(this)

DeliveryDispatcher.assign(order):
  lock(availablePartnersPool)
  try:
    candidates = availablePartnersPool.nearRestaurant(order.restaurant)
    chosen = strategy.selectPartner(order, candidates)
    chosen.status = BUSY               // claim happens inside the same lock as selection
    return chosen
  finally:
    unlock(availablePartnersPool)

R. End-to-End Use Case

customer.placeOrder(cart) → OrderLineItems snapshot current prices → status = PLACED
  │
  ▼
restaurant confirms → PLACED → CONFIRMED → notifier pushes update to customer
  │
  ▼
restaurant marks ready → CONFIRMED → PREPARING → PREPARING → (ready) triggers DeliveryDispatcher.assign()
  │  select-then-claim under lock → partner marked BUSY, order → OUT_FOR_DELIVERY
  ▼
delivery partner reports delivered → OUT_FOR_DELIVERY → DELIVERED → notifier pushes final update

S. Data Structures

StructureUsed ForWhyAlternative & Trade-off
Pool/index of available delivery partners by geographic cellFast "nearest available" candidate lookupAvoids scanning every partner in the city for every assignmentA full scan is fine for a small demo but doesn't hold up at real courier-fleet scale
Enum-backed state table inside each OrderStateO(1) transition legality checkDirect lookup instead of a scattered chain of conditionals—

T. Complexity Analysis

OperationTimeSpace
transitionTo()O(1)O(1)
DeliveryDispatcher.assign() (geo-indexed)O(candidates in nearby cell)O(active partners)
Status notification fan-outO(subscribers) — here, typically O(1), the customer's own deviceO(subscribers)

U. Testing Strategy

V. Interviewer Follow-Ups

Follow-UpTestingDesign ChangeClasses ChangedPattern/Trade-off
"Batch multiple orders from the same restaurant onto one delivery run."Whether Order/DeliveryPartner assumed a strict 1:1 relationshipA DeliveryRun aggregating multiple orders assigned to one partnerNew aggregate class, DeliveryDispatcher updatedTests whether the 1:1 assumption from F was actually isolated behind Dispatcher, not hardcoded elsewhere
"A delivery partner's app is offline and resends the same 'picked up' event three times on reconnect."Whether OrderState transitions were idempotent for duplicate eventsonPickedUp() from an already-PREPARING-derived state is a no-op if already in that state, not an errorOrderState onlySame idempotency-under-retries lesson as Pub-Sub (Problem 10)
"Support live courier location tracking on a map."Whether location updates were conflated with lifecycle status updatesA separate, high-frequency location-update channel distinct from the low-frequency status ObserverNew notifier, Order/OrderState untouchedTests whether status and location were kept as separate concerns

W. Concurrency

Shared mutable state: the pool of currently-available delivery partners, particularly during a lunch-rush spike when many orders become ready for pickup at once.

X. Persistence

Orders and their line-item snapshots are durable, legally significant records — a receipt that changes after the fact or disappears on restart is unacceptable. Delivery-partner availability state is more transient and could reasonably live in a fast in-memory/cache layer, refreshed frequently, while the order and its price snapshot remain firmly repository-backed.

Y. Scaling Boundary

LayerWhat Belongs Here
LLDOrder/OrderLineItem model, state machine, assignment strategy, atomic select-then-claim
HLDReal-time geo-indexing of couriers at city scale, route optimization, cross-restaurant order batching at the platform level
Explicitly out of scopeRoute-optimization algorithms and city-scale geo-infrastructure unless the round pivots there

Z. Failure Scenarios

AA. Common Bad Designs

AB. Design Trade-Offs

DecisionOption AOption BRecommendation
Delivery assignment scopeOne partner per order (1:1)Batched delivery runs (one partner, multiple orders)Start 1:1 for correctness and clarity; batching is a real optimization but adds real complexity — defer until asked
Handling duplicate status eventsReject as an errorSilently no-op if already in the target stateNo-op for exact duplicates (idempotent), reject only for genuinely illegal transitions — distinguishing "already done" from "impossible" avoids false alarms from network retries

AC. Interviewer Evaluation

Strong signal: snapshotting price/name into the order unprompted; building the order lifecycle as a State machine rather than a raw enum; independently identifying the select-then-claim race for delivery-partner assignment; treating duplicate status events as an idempotency question, not just an error case.

Red flag: a live price reference to the catalog; a status field with scattered validation logic; unlocked select-then-claim assignment; assuming status events always arrive once, in order.

AD. Interview Simulation

Interviewer: Design a food delivery system like DoorDash.
Candidate: One thing I'd get right immediately: an order should snapshot each item's name and price at checkout time into an OrderLineItem, not hold a live reference to the restaurant's MenuItem. Otherwise a price change today silently rewrites a receipt from last week.
Interviewer: How do you model the order's progress from placed to delivered?
Candidate: As a State machine — each OrderState knows which events are legal from that specific status. That matters because status updates come from external systems like the restaurant's POS or the courier's phone, over unreliable networks, so I can't trust every incoming event is legal or even novel.
Interviewer: What happens when two orders are ready for pickup at the same moment and there's only one available courier?
Candidate: Selecting a candidate and claiming them has to be one atomic step under a lock — same fix as the elevator dispatcher's double-assignment problem. If I select first and claim later as two separate steps, both orders could pick the same courier before either claim commits.
Interviewer: The courier's app is flaky and sends the same "picked up" event three times.
Candidate: That should be a no-op, not an error — if the order's already past that state, a duplicate of an already-applied event is idempotent by definition. I'd only reject events that are genuinely illegal from the current state, like "delivered" arriving while still "placed."

AE. Online Solution Comparison

SourceApproachStrengthWeaknessWorth Learning
GeeksforGeeks — Design a Food Delivery App like Swiggy/ZomatoEnd-to-end walkthrough covering restaurants, menus, orders, and delivery assignmentBroad functional coverage including restaurant onboarding and search, beyond just the order lifecycleOrder status is generally treated as a simple field update rather than an explicit State machine with transition legality, and price-history snapshotting isn't typically called outGood for the broader functional surface (search, restaurant onboarding); this document's OrderLineItem snapshot and OrderState machine add the correctness depth
ashishps1/awesome-low-level-design (GitHub)A curated open-source repository of worked LLD problems, including food-delivery/order-management-style systemsReal, runnable code examples for cross-checking class shapesBreadth-over-depth — individual problems get a working solution without the same depth of concurrency/idempotency analysisGood for spot-checking entity boundaries against an independent, community-vetted source

AF. Interview Cheat Sheet

ProblemRestaurant / Food Delivery System
DifficultyIntermediate → Advanced
Core requirementsMenu browsing/cart, order lifecycle with live status updates, delivery-partner assignment, price-history correctness
Key clarificationsAssignment rule? Out-of-order/duplicate status events possible? Price-history handling? Cancellation window?
Core entitiesMenuItem, OrderLineItem, Order, OrderState, DeliveryPartner, DeliveryAssignmentStrategy, DeliveryDispatcher, OrderStatusNotifier, OrderService
Key relationshipsMenuItem/OrderLineItem snapshot split; State machine for lifecycle; Strategy for assignment; Observer for status push
Key patternsState, Strategy, Observer
Most important decisionFreezing price/name into OrderLineItem at checkout instead of a live catalog reference
Likely-changing behaviorAssignment rule, additional lifecycle states, notification channels
Concurrency concernAtomic select-then-claim for delivery-partner assignment; idempotent handling of duplicate/out-of-order status events
Top follow-upsBatched delivery runs, duplicate event idempotency, live location tracking as a separate channel
Common mistakesLive price reference instead of a snapshot; raw status enum with scattered checks; unlocked select-then-claim; trusting every status event as legal and novel
What NOT to over-engineerDon't design route optimization or city-scale geo-infrastructure unless asked

AG. Full Implementation

A complete, compilable reference implementation of every class in the diagram above (composition, inheritance, interface realization, and dependency-injected strategies all preserved per section I), with a runnable demo exercising the primary flow — in Java, C++, JavaScript, and TypeScript.

import java.util.*;

/**
 * Problem 18: Restaurant / Food Delivery System (single-threaded -- no concurrency badge)
 *
 * Key design decisions faithfully reproduced from the class diagram:
 *  - MenuItem (live, editable catalog) vs OrderLineItem (frozen price/name snapshot at checkout).
 *  - OrderState hierarchy: PLACED -> CONFIRMED -> PREPARING -> OUT_FOR_DELIVERY -> DELIVERED, each
 *    state only accepting its own legal next event; illegal/duplicated events are rejected by the state.
 *  - DeliveryAssignmentStrategy is injected (Strategy); DeliveryDispatcher does the atomic select-then-claim.
 *  - OrderStatusNotifier is a thin Observer subject fanning out every legal transition.
 *  - OrderService is the facade: placeOrder() / updateStatus().
 */
public class p18 {

    enum OrderStatus { PLACED, CONFIRMED, PREPARING, OUT_FOR_DELIVERY, DELIVERED, CANCELLED }
    enum StatusEvent { CONFIRM, PREPARE, PICKUP, DELIVER, CANCEL }
    enum PartnerStatus { AVAILABLE, BUSY }

    static final class Customer {
        final String id, name, address;
        Customer(String id, String name, String address) { this.id = id; this.name = name; this.address = address; }
    }

    static final class Restaurant {
        final String id, name, location;
        final List<MenuItem> menu = new ArrayList<>();
        Restaurant(String id, String name, String location) { this.id = id; this.name = name; this.location = location; }
        void addMenuItem(MenuItem item) { menu.add(item); }
    }

    // ---------- MenuItem (live catalog) vs OrderLineItem (frozen snapshot) ----------

    static final class MenuItem {
        final String id;
        String name;
        double price;
        boolean available;
        MenuItem(String id, String name, double price) { this.id = id; this.name = name; this.price = price; this.available = true; }
    }

    static final class OrderLineItem {
        final String nameAtOrderTime;
        final double priceAtOrderTime;
        final int quantity;
        OrderLineItem(MenuItem item, int quantity) {
            // Freeze name/price at checkout -- a later price change must never rewrite a past receipt.
            this.nameAtOrderTime = item.name;
            this.priceAtOrderTime = item.price;
            this.quantity = quantity;
        }
        double subtotal() { return priceAtOrderTime * quantity; }
    }

    static final class CartItem {
        final MenuItem menuItem;
        final int quantity;
        CartItem(MenuItem menuItem, int quantity) { this.menuItem = menuItem; this.quantity = quantity; }
    }

    static final class Cart {
        final List<CartItem> items = new ArrayList<>();
        void addItem(MenuItem item, int quantity) { items.add(new CartItem(item, quantity)); }
    }

    // ---------- OrderState (State pattern) ----------

    interface OrderState {
        OrderState onConfirm();
        OrderState onPrepared();
        OrderState onPickedUp();
        OrderState onDelivered();
        OrderStatus status();
    }

    static final class PlacedState implements OrderState {
        static final PlacedState INSTANCE = new PlacedState();
        public OrderState onConfirm() { return ConfirmedState.INSTANCE; }
        public OrderState onPrepared() { throw new IllegalStateException("Cannot prepare before confirmation"); }
        public OrderState onPickedUp() { throw new IllegalStateException("Cannot pick up before confirmation"); }
        public OrderState onDelivered() { throw new IllegalStateException("Cannot deliver before confirmation"); }
        public OrderStatus status() { return OrderStatus.PLACED; }
    }

    static final class ConfirmedState implements OrderState {
        static final ConfirmedState INSTANCE = new ConfirmedState();
        public OrderState onConfirm() { throw new IllegalStateException("Already confirmed"); }
        public OrderState onPrepared() { return PreparingState.INSTANCE; }
        public OrderState onPickedUp() { throw new IllegalStateException("Cannot pick up before preparation"); }
        public OrderState onDelivered() { throw new IllegalStateException("Cannot deliver before preparation"); }
        public OrderStatus status() { return OrderStatus.CONFIRMED; }
    }

    static final class PreparingState implements OrderState {
        static final PreparingState INSTANCE = new PreparingState();
        public OrderState onConfirm() { throw new IllegalStateException("Already confirmed"); }
        public OrderState onPrepared() { throw new IllegalStateException("Already preparing"); }
        public OrderState onPickedUp() { return OutForDeliveryState.INSTANCE; }
        public OrderState onDelivered() { throw new IllegalStateException("Cannot deliver before pickup"); }
        public OrderStatus status() { return OrderStatus.PREPARING; }
    }

    static final class OutForDeliveryState implements OrderState {
        static final OutForDeliveryState INSTANCE = new OutForDeliveryState();
        public OrderState onConfirm() { throw new IllegalStateException("Already confirmed"); }
        public OrderState onPrepared() { throw new IllegalStateException("Already prepared"); }
        public OrderState onPickedUp() { throw new IllegalStateException("Already picked up"); }
        public OrderState onDelivered() { return DeliveredState.INSTANCE; }
        public OrderStatus status() { return OrderStatus.OUT_FOR_DELIVERY; }
    }

    static final class DeliveredState implements OrderState {
        static final DeliveredState INSTANCE = new DeliveredState();
        public OrderState onConfirm() { throw new IllegalStateException("Order already delivered -- terminal state"); }
        public OrderState onPrepared() { throw new IllegalStateException("Order already delivered -- terminal state"); }
        public OrderState onPickedUp() { throw new IllegalStateException("Order already delivered -- terminal state"); }
        public OrderState onDelivered() { throw new IllegalStateException("Order already delivered -- terminal state"); }
        public OrderStatus status() { return OrderStatus.DELIVERED; }
    }

    // ---------- Order ----------

    static final class Order {
        final String id;
        final Customer customer;
        final Restaurant restaurant;
        final List<OrderLineItem> lineItems;
        private OrderState state = PlacedState.INSTANCE;
        private final OrderStatusNotifier notifier;
        DeliveryPartner assignedPartner;

        Order(String id, Customer customer, Restaurant restaurant, List<OrderLineItem> lineItems, OrderStatusNotifier notifier) {
            this.id = id; this.customer = customer; this.restaurant = restaurant; this.lineItems = lineItems; this.notifier = notifier;
        }

        OrderStatus getStatus() { return state.status(); }

        double totalPrice() {
            double total = 0;
            for (OrderLineItem li : lineItems) total += li.subtotal();
            return total;
        }

        /** Delegates transition legality to the current state; illegal events are rejected by the state itself. */
        void transitionTo(StatusEvent event) {
            OrderState next;
            switch (event) {
                case CONFIRM: next = state.onConfirm(); break;
                case PREPARE: next = state.onPrepared(); break;
                case PICKUP: next = state.onPickedUp(); break;
                case DELIVER: next = state.onDelivered(); break;
                default: throw new IllegalArgumentException("Unsupported event via transitionTo: " + event);
            }
            state = next;
            notifier.notifyStatusChanged(this);
        }

        void cancel() {
            if (state == DeliveredState.INSTANCE) throw new IllegalStateException("Cannot cancel a delivered order");
            state = new OrderState() {
                public OrderState onConfirm() { throw new IllegalStateException("Order is cancelled"); }
                public OrderState onPrepared() { throw new IllegalStateException("Order is cancelled"); }
                public OrderState onPickedUp() { throw new IllegalStateException("Order is cancelled"); }
                public OrderState onDelivered() { throw new IllegalStateException("Order is cancelled"); }
                public OrderStatus status() { return OrderStatus.CANCELLED; }
            };
            notifier.notifyStatusChanged(this);
        }
    }

    // ---------- Delivery assignment ----------

    static final class DeliveryPartner {
        final String id, name;
        PartnerStatus status = PartnerStatus.AVAILABLE;
        String location;
        DeliveryPartner(String id, String name, String location) { this.id = id; this.name = name; this.location = location; }

        /** Claims this partner for a delivery. Caller (DeliveryDispatcher) must already hold the pool lock. */
        void claim() {
            if (status != PartnerStatus.AVAILABLE) throw new IllegalStateException("Partner " + id + " is not available");
            status = PartnerStatus.BUSY;
        }

        void release() { status = PartnerStatus.AVAILABLE; }
    }

    interface DeliveryAssignmentStrategy {
        DeliveryPartner selectPartner(Order order, List<DeliveryPartner> candidates);
    }

    /** Picks the first available partner whose recorded location matches the restaurant's -- a simple "nearest" proxy. */
    static final class NearestAvailablePartnerStrategy implements DeliveryAssignmentStrategy {
        public DeliveryPartner selectPartner(Order order, List<DeliveryPartner> candidates) {
            for (DeliveryPartner p : candidates) {
                if (p.location.equals(order.restaurant.location)) return p;
            }
            if (candidates.isEmpty()) throw new NoSuchElementException("No available delivery partners");
            return candidates.get(0); // fall back to any available partner
        }
    }

    /** Atomically selects and claims a partner -- selection and claim happen under the same lock. */
    static final class DeliveryDispatcher {
        private final List<DeliveryPartner> partnerPool;
        private final DeliveryAssignmentStrategy strategy;

        DeliveryDispatcher(List<DeliveryPartner> partnerPool, DeliveryAssignmentStrategy strategy) {
            this.partnerPool = partnerPool;
            this.strategy = strategy;
        }

        synchronized DeliveryPartner assign(Order order) {
            List<DeliveryPartner> candidates = new ArrayList<>();
            for (DeliveryPartner p : partnerPool) if (p.status == PartnerStatus.AVAILABLE) candidates.add(p);
            DeliveryPartner chosen = strategy.selectPartner(order, candidates);
            chosen.claim(); // claim happens inside the same lock as selection -- no double-assignment race
            order.assignedPartner = chosen;
            return chosen;
        }
    }

    // ---------- OrderStatusNotifier (Observer) ----------

    interface OrderStatusObserver {
        void onStatusChanged(Order order);
    }

    static final class OrderStatusNotifier {
        private final List<OrderStatusObserver> observers = new ArrayList<>();
        void subscribe(OrderStatusObserver observer) { observers.add(observer); }
        void notifyStatusChanged(Order order) { for (OrderStatusObserver o : observers) o.onStatusChanged(order); }
    }

    static final class ConsoleStatusObserver implements OrderStatusObserver {
        public void onStatusChanged(Order order) {
            System.out.printf("[push] Order %s status -> %s%n", order.id, order.getStatus());
        }
    }

    // ---------- OrderService (facade) ----------

    static final class OrderService {
        private final DeliveryDispatcher dispatcher;
        private final OrderStatusNotifier notifier;
        private final Map<String, Order> orders = new HashMap<>();
        private int orderSeq = 0;

        OrderService(DeliveryDispatcher dispatcher, OrderStatusNotifier notifier) {
            this.dispatcher = dispatcher;
            this.notifier = notifier;
        }

        Order placeOrder(Customer customer, Restaurant restaurant, Cart cart) {
            List<OrderLineItem> lineItems = new ArrayList<>();
            for (CartItem ci : cart.items) lineItems.add(new OrderLineItem(ci.menuItem, ci.quantity)); // freeze price/name now
            String id = "ORD-" + (++orderSeq);
            Order order = new Order(id, customer, restaurant, lineItems, notifier);
            orders.put(id, order);
            notifier.notifyStatusChanged(order); // announce the initial PLACED state too
            return order;
        }

        void updateStatus(String orderId, StatusEvent event) {
            Order order = orders.get(orderId);
            if (order == null) throw new NoSuchElementException("Unknown order " + orderId);
            if (event == StatusEvent.CANCEL) { order.cancel(); return; }
            if (event == StatusEvent.PICKUP) dispatcher.assign(order); // assign a courier right before pickup
            order.transitionTo(event);
        }
    }

    // ---------- Demo ----------

    public static void main(String[] args) {
        Restaurant restaurant = new Restaurant("R1", "Curry House", "Downtown");
        MenuItem biryani = new MenuItem("M1", "Chicken Biryani", 12.50);
        MenuItem naan = new MenuItem("M2", "Garlic Naan", 3.00);
        restaurant.addMenuItem(biryani);
        restaurant.addMenuItem(naan);

        OrderStatusNotifier notifier = new OrderStatusNotifier();
        notifier.subscribe(new ConsoleStatusObserver());

        List<DeliveryPartner> partners = List.of(
                new DeliveryPartner("P1", "Sam", "Downtown"),
                new DeliveryPartner("P2", "Robin", "Uptown"));
        DeliveryDispatcher dispatcher = new DeliveryDispatcher(partners, new NearestAvailablePartnerStrategy());

        OrderService orderService = new OrderService(dispatcher, notifier);

        Customer customer = new Customer("C1", "Alice", "12 Elm St");
        Cart cart = new Cart();
        cart.addItem(biryani, 2);
        cart.addItem(naan, 1);

        Order order = orderService.placeOrder(customer, restaurant, cart);
        System.out.printf("Order placed: %s, total=%.2f%n", order.id, order.totalPrice());

        // Live menu price changes after checkout must NOT affect this already-placed order's receipt.
        biryani.price = 15.00;
        System.out.printf("Menu price changed to %.2f, but order total is still %.2f%n", biryani.price, order.totalPrice());

        orderService.updateStatus(order.id, StatusEvent.CONFIRM);
        orderService.updateStatus(order.id, StatusEvent.PREPARE);
        orderService.updateStatus(order.id, StatusEvent.PICKUP); // triggers atomic partner assignment
        System.out.println("Assigned partner: " + order.assignedPartner.name);
        orderService.updateStatus(order.id, StatusEvent.DELIVER);

        // Illegal transition: a duplicated/out-of-order DELIVER event on an already-delivered order must be rejected.
        try {
            orderService.updateStatus(order.id, StatusEvent.DELIVER);
        } catch (IllegalStateException e) {
            System.out.println("Rejected duplicate DELIVER event: " + e.getMessage());
        }

        // A second order competing for the same restaurant-local partner must not double-assign that partner.
        Order order2 = orderService.placeOrder(customer, restaurant, cart);
        orderService.updateStatus(order2.id, StatusEvent.CONFIRM);
        orderService.updateStatus(order2.id, StatusEvent.PREPARE);
        // P1 was claimed (BUSY) for order1 and is never auto-released on delivery in this simplified model,
        // so the dispatcher correctly falls back to the only remaining available partner, P2.
        orderService.updateStatus(order2.id, StatusEvent.PICKUP);
        System.out.println("Order2 assigned partner: " + order2.assignedPartner.name);

        // Direct PLACED -> DELIVERED must never be allowed.
        Order order3 = orderService.placeOrder(customer, restaurant, cart);
        try {
            orderService.updateStatus(order3.id, StatusEvent.DELIVER);
        } catch (IllegalStateException e) {
            System.out.println("Rejected out-of-order DELIVER on a freshly placed order: " + e.getMessage());
        }
    }
}