CASE STUDY

Coffee Ordering System with Real-Time Notifications

4 min read·711 words·Beginner

Asked at

2 candidate reports between Jan 2026 and Feb 2026

How to use this case study

SDE-2 / Mid

Design the order lifecycle (placed → in progress → ready → picked up), the data model, APIs, and how the customer is notified when the order is ready.

SDE-3 / Senior

Handle concurrent updates to orders, the barista queue per store, reliable notifications with retries, and scaling from one shop to thousands.

Staff / Principal

Discuss store-level partitioning, offline tolerance for store devices, morning-peak capacity, and extensibility (loyalty, delivery).


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 (or cancelled).
  • 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}/menu
  • POST /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

  1. Validate items and options against the store's menu (price computed on the server, never trusted from the client).
  2. Authorize payment. Save the order placed with an ETA based on the current queue length.
  3. Publish order_placed. The barista tablet shows it at the end of the queue.

5.2 Barista updates

  1. The barista taps "start" → in_progress, then "ready".
  2. 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).
  3. ready triggers 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

DecisionChoiceWhyAlternative
Status fan-outEvents → queue view, notifications, live statusLoose coupling, reliable retriesDirect calls from API: fragile
ConcurrencyVersion checks + state machineClean statesLast write wins: confusing states
NotificationsPush with SMS fallback, dedupedReliable, no spamOnly in-app: users miss it
PartitioningBy storeMatches access patternGlobal 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.

More Case Studies

Practice with a Mock Interview

Apply what you learned in a live system design mock interview with our AI interviewer.

Start System Design Interview →