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