0) Problem Restatement
Design a coffee ordering system, first for one coffee shop, then for a chain of thousands of stores (asked at Salesforce, time-boxed). Customers browse the menu, customize drinks ("oat milk, extra shot"), pay, and choose pickup or dine-in. Baristas see a queue of orders and move each through states. The customer gets a notification when the order is ready.
1) Requirements
1.1 Functional
- Menu per store (items, sizes, options, prices, availability).
- Place and pay for an order for a specific store.
- Barista queue: see orders in order, and move them
placed → in_progress → ready → picked_up(orcancelled). - Real-time status for the customer, plus a push notification when ready.
1.2 Non-Functional
- Morning peak: many orders in a short window.
- An order must never be lost or made twice.
- Status updates are reliable and fast (seconds).
- Store devices may have flaky internet.
1.3 Scale Estimates
- 5,000 stores × 500 orders/day = 2.5M orders/day, with a peak of ~300 orders/sec around 8 AM.
2) API Design
GET /v1/stores/{id}/menuPOST /v1/orders(Idempotency-Key){ store_id, items: [{ item_id, size, options }], pickup: true }→{ order_id, status: "placed", eta }GET /v1/stores/{id}/queue?status=placed,in_progress(barista)POST /v1/orders/{id}/status{ status: "ready", expected_version: 3 }- WebSocket or SSE
/v1/orders/{id}/events(customer live status)
3) Architecture
Architecture Diagram
flowchart LR
CU["Customer app"] --> API["Order API"]
API --> PAY["Payments"]
API --> DB[("Orders DB - partitioned by store")]
API --> K[("Order events")]
BA["Barista tablet"] --> API
K --> Q["Store queue updater"]
Q --> BA
K --> NS["Notification Service"]
NS -->|"push / SMS"| CU
K --> RT["Realtime status - WebSocket"]
RT --> CU- Order API: validates against the menu, takes payment (authorize, capture when ready), and saves the order.
- Orders DB: partitioned by
store_id, since queries are almost always per store. - Order events (Kafka or a queue): every state change is published. The barista queue, notifications and live status all consume it.
- Notification Service: push first, SMS fallback. It retries and deduplicates by
(order_id, status).
4) Data Model
stores: store_id, name, timezone, open_hours
menu_items: store_id, item_id, name, base_price_cents, options (JSON), available
orders: order_id, store_id, customer_id, status, items (JSON), total_cents,
placed_at, ready_at, picked_up_at, version, idempotency_key
order_events: order_id, from_status, to_status, actor, ts
5) Key Flows
5.1 Placing an order
- Validate items and options against the store's menu (price computed on the server, never trusted from the client).
- Authorize payment. Save the order
placedwith an ETA based on the current queue length. - Publish
order_placed. The barista tablet shows it at the end of the queue.
5.2 Barista updates
- The barista taps "start" →
in_progress, then "ready". - Each update uses optimistic concurrency (
expected_version), so two baristas tapping at once don't create a confusing state. The state machine rejects invalid moves (e.g.,picked_up → in_progress). readytriggers a push notification ("Your oat latte is ready at the counter") and a live update in the app.
6) From One Shop to a Chain
- One shop: a single service and DB is enough. The tablet polls every few seconds.
- Chain: partition data and queues by store, make services stateless and horizontally scaled, use a CDN-cached menu per store, and pre-scale for the morning peak.
- Store devices offline: the tablet keeps a local copy of the queue and queues status changes, syncing when back online. Online orders for that store may be paused ("store is temporarily not accepting mobile orders").
- Throttling: if a store's queue is too long, show longer ETAs or temporarily limit mobile orders for that store.
7) Trade-offs & Alternatives
| Decision | Choice | Why | Alternative |
|---|---|---|---|
| Status fan-out | Events → queue view, notifications, live status | Loose coupling, reliable retries | Direct calls from API: fragile |
| Concurrency | Version checks + state machine | Clean states | Last write wins: confusing states |
| Notifications | Push with SMS fallback, deduped | Reliable, no spam | Only in-app: users miss it |
| Partitioning | By store | Matches access pattern | Global table: hot and harder to scale |
8) Wrap-Up
Validate and price orders on the server, take payment, and store orders partitioned by store with a strict state machine and version checks. Publish every state change as an event that drives the barista queue, live customer status (WebSocket/SSE) and a deduplicated push notification when the order is ready. Scale from one shop to a chain by partitioning per store, caching menus, pre-scaling for mornings, and letting store tablets work offline.