Vasu Patel logoVasu Patel
← Whiteboard to Blueprint: HLD & LLD
All chapters in this track
  1. 01Chapter 1: What HLD and LLD Are, and How They Differ
  2. 02Chapter 2: Gathering and Clarifying Requirements Before Designing Anything
  3. 03Chapter 3: HLD Deliverables: Architecture, Components, Technology Choices, Data Flow
  4. 04Chapter 4: LLD Deliverables: Class Diagrams and Sequence Diagrams
  5. 05Chapter 5: LLD Deliverables: Database Schema and API Contracts
  6. 06Chapter 6: Documenting Decisions: ADRs and Communicating Trade-offs to Stakeholders
  7. 07Chapter 7: Moving Cleanly from HLD to LLD Without Losing Intent
  8. 08Chapter 8: Common Mistakes: Over-Designing, Under-Specifying, and Skipping Non-Functional Requirements
  9. 09Chapter 9: When LLD Contradicts Its Own HLD
  10. 10Chapter 10: HLD and LLD in Design Reviews: What Reviewers Actually Look For
  11. 11Chapter 11: Worked Example: Ride-Sharing Dispatch — Requirements Through HLD to LLD
  12. 12Chapter 12: Worked Example: Hotel Booking — Requirements Through HLD to LLD
  13. 13Chapter 13: Worked Example: Food Delivery — Requirements Through HLD to LLD
  14. 14Chapter 14: How HLD and LLD Show Up in Interviews and Design Reviews
  15. 15Chapter 15: Common Interview Questions About HLD and LLD

Chapter 13 of 15

Chapter 13: Worked Example: Food Delivery — Requirements Through HLD to LLD

Why this matters

The food delivery worked example closes this book's trio with a genuinely different hard problem: coordinating three parties (customer, restaurant, delivery driver) through a shared order lifecycle, where the order's state must stay consistent across everyone watching it — a multi-party state-synchronization problem neither the ride-sharing nor hotel booking examples required in quite this shape.

Requirements (Chapter 2)

Clarifying questions: "Does the restaurant need to explicitly accept/reject an order, or is acceptance automatic? Should the customer see live order status updates? How is a driver assigned — similar to ride-sharing's nearest-match, or some other mechanism? What's the expected scale?"

Assumed answers: restaurants explicitly accept or reject within a time limit; customers see live status updates; driver assignment uses nearest-available-driver logic similar to Chapter 11's ride-sharing dispatch (explicitly reusing that already-solved sub-problem rather than redesigning it from scratch); assume 5,000 restaurants, ~300,000 orders/day system-wide.

HLD

Architecture diagram:

+----------+     +----------------+     +-------------------+
|  Customer|---->| API Gateway    |---->| Order Service       |
|  App     |     +----------------+     +-------------------+
+----------+              ^                       |
     ^                    |            +-----------+-----------+
     |                    |            v                        v
     |          +-------------------+      +-------------------+
     |          | Restaurant App     |      | Dispatch Service    |
     |          | (accept/reject)    |      | (reuses Chapter 11's |
     |          +-------------------+      |  nearest-driver logic)|
     |                                      +-------------------+
     |                                                |
     +---------------(status updates, WebSocket)------+

Component responsibilities: Order Service owns the order's overall lifecycle state and is the single source of truth all three parties observe; Dispatch Service (reused from Chapter 11's design) owns matching the order to a driver once the restaurant accepts.

Technology choice, with reasoning: "WebSockets (companion Networking book, Chapter 6) for pushing live status updates to the customer app, specifically because polling for status changes would add unacceptable latency to what should feel like a real-time experience, and the persistent-connection overhead is justified here by genuinely frequent status transitions during an active order's lifetime."

Data flow — "place an order":

1. Customer places an order; Order Service creates it in "pending_restaurant_acceptance" state.
2. Restaurant App receives the order; restaurant accepts or rejects within
   a time limit (e.g., 3 minutes).
3. On acceptance: Order Service transitions to "preparing" and requests a
   driver from Dispatch Service (Chapter 11's logic).
4. Driver assigned -> order transitions to "driver_assigned" -> "picked_up"
   -> "delivered", each transition pushed to the customer via WebSocket.
5. On rejection (or timeout): order transitions to "rejected", customer
   notified, no charge is captured.

LLD — deep-dive on the order state machine (the genuinely hardest part)

Why this is the hardest part: "The tricky part isn't any single state transition — it's making sure the order's state transitions are valid (you can't go from 'delivered' back to 'preparing') and that every party watching the order sees a CONSISTENT view of its current state, even though three different actors can potentially trigger transitions."

This is a direct, real-world application of the companion Design Patterns book's Chapter 11 State pattern:

class OrderState(ABC):
    @abstractmethod
    def accept(self, order: "Order") -> None: ...
    @abstractmethod
    def reject(self, order: "Order") -> None: ...
    @abstractmethod
    def mark_delivered(self, order: "Order") -> None: ...

class PendingAcceptanceState(OrderState):
    def accept(self, order: "Order") -> None:
        order.set_state(PreparingState())
        order.request_driver()  # triggers Dispatch Service
    def reject(self, order: "Order") -> None:
        order.set_state(RejectedState())
    def mark_delivered(self, order: "Order") -> None:
        raise InvalidTransitionError("Cannot deliver an order not yet accepted")

class PreparingState(OrderState):
    def accept(self, order: "Order") -> None:
        raise InvalidTransitionError("Already accepted")
    def reject(self, order: "Order") -> None:
        raise InvalidTransitionError("Cannot reject after acceptance")
    def mark_delivered(self, order: "Order") -> None:
        order.set_state(DeliveredState())  # (intermediate states omitted for brevity)

Sequence diagram — ensuring consistent state visibility across three parties:

Restaurant       OrderService        Database         Customer(WebSocket)
    |                  |                  |                    |
    |--accept()------->|                  |                    |
    |                  |--UPDATE status-->|                    |
    |                  |<--committed------|                    |
    |                  |--publish "order.accepted" event------>|
    |                  |                  |         (customer's app
    |                  |                  |          receives push update)
    |<--200 OK---------|                  |                    |

Why the database update happens before the event publish: "The state transition is committed to the database — the source of truth — before the event notifying the customer is published, directly the companion System Design book's Chapter 8 pattern: the durable state change happens on the synchronous path, and the notification (which could theoretically fail to deliver, e.g., a dropped WebSocket) happens afterward, off the critical path — this ordering ensures the database is never inconsistent with what actually happened, even if a specific customer's live update happens to fail to deliver."

Common mistakes

  1. Allowing invalid state transitions (accepting an order that's already been rejected) by not modeling the order lifecycle as an explicit state machine — using a plain string status field with ad hoc conditional checks scattered across the codebase reintroduces exactly the OOP book's Chapter 11 anti-pattern the State pattern exists to prevent.
  2. Publishing the customer-facing status update event before the database transition commits, risking a customer being notified of a state change that then fails to actually persist (e.g., a subsequent constraint violation) — the durable state change should always precede the notification, not follow it.
  3. Redesigning driver-matching logic from scratch rather than recognizing and reusing Chapter 11's already-solved nearest-driver-matching sub-problem — missing an opportunity for genuine design reuse across worked examples that, in a real organization, might reasonably share an actual internal library or service.
  4. Not accounting for a restaurant's acceptance timeout explicitly in the state machine, leaving an ambiguous "restaurant never responded" case unhandled.

Chapter summary / cheat sheet

  • Food delivery's genuinely hard problem: a three-party (customer, restaurant, driver) shared order lifecycle requiring both valid-transition enforcement and consistent state visibility across all observers.
  • The State pattern (companion Design Patterns book, Chapter 11) is the direct, correct LLD tool for enforcing valid order transitions structurally, rather than a string status field with scattered conditional checks.
  • Commit the durable state change to the database before publishing the customer-facing notification event — the source of truth must never lag behind, or become inconsistent with, what's been communicated to an observer.
  • This worked example deliberately reused Chapter 11's driver-matching sub-problem rather than re-solving it, demonstrating that HLD/LLD work across related systems can and should build on previously-solved components rather than starting from zero each time.