ExpertGeospatialDispatchWebSocketsSagasIdempotency

Food Delivery Platform

Coordinate customers, restaurants, couriers, menus, dispatch, and live delivery state.

Product overview

Move a prepared meal from an independently operated restaurant to a customer while coordinating volatile menus, preparation, courier dispatch, location, payment, and support.

Main actors

Customer, Restaurant, Courier, Support.

Assumptions

  • Restaurants control opening state and item availability
  • Couriers opt into offers
  • Location updates are approximate and short-lived
  • Example workload: a city may see 200 active deliveries, purely for design discussion

Non-goals

  • Owning restaurant POS systems
  • Guaranteeing exact ETAs
  • Autonomous routing
  • Storing raw card data

Requirements

Must-have

  • Discover open restaurants in a service zone
  • Order against a versioned menu
  • Restaurant accept/reject workflow
  • Offer and assign a courier atomically
  • Track active delivery and settle participants

Should-have

  • Scheduled orders
  • Substitutions
  • Customer-courier masked chat
  • Operational dispatch console

Future scope

  • Batch deliveries
  • Restaurant POS adapters
  • Demand forecasting
  • Multi-stop routing

Non-functional decisions

ConcernDecision
AvailabilityBrowse may serve slightly stale data; accepting orders and assignments fail closed when ownership cannot be established.
LatencyDispatch candidates and location fan-out are latency-sensitive; settlement is asynchronous.
ConsistencyOrder state and courier assignment require conditional transitions; map position and ETA are eventual.
PrivacyPrecise courier/customer locations are retained briefly and exposed only during an active delivery.
AuditabilityEvery manual reassignment, refund, and state override records actor and reason.

User roles

RoleAccess boundary
CustomerOwn orders and active courier tracking
Restaurant staffOnly their restaurant's menu and incoming orders
CourierOwn offers, active job, and minimum pickup/drop-off details
Dispatcher/supportRegion-scoped operations; sensitive actions audited

Core workflows

Place order
Resolve delivery address and service zone → Fetch menu version → Validate items and restaurant state → Calculate quote → Authorize payment → Create order → Ask restaurant to accept → Start dispatch after acceptance
Dispatch
Select nearby eligible couriers → Create short-lived offers → First valid acceptance wins → Notify losers → Stream pickup route → Reassign on timeout or rejection
Complete delivery
Courier confirms pickup → Location updates feed customer projection → Courier supplies proof of delivery → Capture/settle payment → Calculate courier and restaurant payable

Domain model

EntitiesRelationship
Restaurant → Menu → MenuItemA published menu version is priced at order time
Order → OrderLineOrder stores immutable names, modifiers, taxes, and prices
Order → DeliveryAccepted order creates one delivery state machine
Delivery → CourierOfferMany offers may exist; at most one can win
Courier → LocationSampleEphemeral time-series data, not the operational source of truth

Database design

PostgreSQL remains the transactional source of truth. Add specialized stores only for the workloads called out below.

restaurants — Merchant and service state

restaurants ----------- id UUID PK name TEXT status TEXT service_area GEOGRAPHY timezone TEXT menu_version BIGINT Constraints: valid timezone; status enum check Indexes: GIST(service_area) for address containment; (status)

orders — Commercial and kitchen workflow

orders ------ id UUID PK customer_id UUID FK restaurant_id UUID FK status TEXT menu_version BIGINT total_minor BIGINT delivery_address JSONB version INT Constraints: total_minor >= 0 Indexes: (restaurant_id, status, created_at); (customer_id, created_at DESC)

deliveries — Pickup/drop-off state and winner

deliveries ---------- id UUID PK order_id UUID FK UNIQUE courier_id UUID FK NULL status TEXT pickup_by TIMESTAMPTZ version INT Constraints: UNIQUE(order_id) Indexes: (courier_id, status); (status, pickup_by)

courier_offers — Expiring dispatch proposals

courier_offers -------------- id UUID PK delivery_id UUID FK courier_id UUID FK status TEXT expires_at TIMESTAMPTZ Constraints: UNIQUE(delivery_id, courier_id) Indexes: (courier_id, status, expires_at)

ledger_entries — Immutable order settlement components

ledger_entries -------------- id UUID PK order_id UUID FK account_ref TEXT amount_minor BIGINT currency CHAR(3) entry_type TEXT Constraints: amount_minor <> 0; UNIQUE(order_id, account_ref, entry_type) Indexes: (account_ref, id)

Transaction and retention boundaries

  • Create order snapshot and outbox after authorization result is known
  • Courier acceptance conditionally updates offer and delivery where courier_id IS NULL; database winner decides
  • Delivery completion and balanced settlement entries commit together
  • Location history uses retention deletion; orders and settlement are immutable/audited

API design

GET /v1/restaurants?lat=…&lng=…&cursor=… POST /v1/orders/quote POST /v1/orders POST /v1/restaurant-orders/:id/accept POST /v1/courier-offers/:id/accept PATCH /v1/deliveries/:id/status
EndpointPurpose and permissionIdempotency / status
GET /v1/restaurants?lat=…&lng=…&cursor=…Discover serviceable open restaurants
Public with rate limits
Not required
200, 400
POST /v1/orders/quoteReturn expiring server-calculated quote
Customer
Not required
200, 409, 422
POST /v1/ordersAuthorize and place order
Customer
Required
201, 202, 402, 409
POST /v1/restaurant-orders/:id/acceptAccept with promised preparation time
Restaurant staff for owning restaurant
State transition is idempotent
200, 409
POST /v1/courier-offers/:id/acceptAttempt to win delivery assignment
Offer recipient
Required; atomic winner check
200, 409, 410
PATCH /v1/deliveries/:id/statusAdvance permitted delivery state
Assigned courier
Transition id supplied
200, 409, 422

List endpoints use opaque keyset cursors unless a small bounded collection is explicit. Unknown objects return 404 where 403 would leak existence.

Architecture

System context

Food Delivery Platform system context
Food Delivery Platform system context

Main application architecture

Food Delivery Platform application architecture
Food Delivery Platform application architecture

Critical workflow sequence

Food Delivery Platform workflow sequence
Food Delivery Platform workflow sequence

Event model

EventProducer → consumersDelivery
RestaurantAcceptedOrderOrder service → Dispatch, notificationsOutbox; duplicate-safe dispatch creation
CourierAssignedDispatch → Order projection, realtime gatewayAt-least-once with delivery version
DeliveryCompletedDelivery → Settlement, receipts, analyticsSettlement uses event-id inbox; dead-letter pages operations

Failure modes

Menu changed

Failure
Item becomes unavailable after customer viewed it.

Mitigation
Quote and order validate menu version and availability; return a resolvable 409 diff.

Two couriers accept

Failure
Offers are accepted concurrently.

Mitigation
Single conditional delivery UPDATE or row lock elects exactly one winner; notify the loser.

Restaurant silent

Failure
Authorized order is not accepted before deadline.

Mitigation
Durable timer cancels order, voids authorization, and reports pending void state if provider is down.

Location stops

Failure
Courier app loses connectivity.

Mitigation
Show last-updated time and widen ETA; workflow status never depends on continuous GPS.

Duplicate completion

Failure
Courier retries completion after timeout.

Mitigation
Transition id and version make completion and settlement creation idempotent.

Settlement worker crash

Failure
Some participants appear unpaid.

Mitigation
Balanced entries commit atomically; provider payout is separately retried and reconciled.

Consistency and concurrency

Strong consistency / transactions

  • Order price snapshot
  • Restaurant acceptance transition
  • Single courier assignment
  • Balanced settlement entries

Eventual convergence

  • Courier map position
  • ETA
  • Notifications
  • Restaurant ranking

Concurrency controls

  • Conditional UPDATE delivery SET courier_id = ? WHERE courier_id IS NULL elects the winner
  • Versioned state transitions prevent pickup before assignment
  • Unique settlement component keys prevent double credit
  • No distributed lock is needed for a delivery owned by one PostgreSQL shard

Caching and background work

Safe to cache

  • Restaurant discovery results by coarse geocell
  • Published versioned menus
  • Static restaurant media

Never a cache authority

  • Current courier assignment
  • Whether restaurant can accept this order
  • Settlement balance

Short TTL discovery cache; menu keys include version. Invalidation accelerates closure changes, while order placement revalidates every assumption.

Background jobs

  • Restaurant acceptance deadline
  • Offer expiry and redispatch
  • Push notifications
  • Payment void/capture reconciliation
  • Settlement and payout
  • Location retention cleanup

Security boundaries

Security claims here are design controls, not certification. Threat modeling and jurisdiction-specific review remain required.

  • Object-level authorization binds restaurant and courier resources to caller
  • Courier sees customer address only for assigned active delivery
  • Location access is short-lived, encrypted, and audited
  • Signed, deduplicated provider webhooks
  • Server recomputes all prices, fees, and payable amounts
  • Rate-limit order creation, offers, and location ingestion independently
  • Support overrides require reason and step-up authentication

Observability

Logs

  • Restaurant acceptance, offer, assignment, pickup, and completion transitions with order/delivery ids
  • Candidate-filter reasons and courier-offer expiry without storing raw location history in logs
  • Settlement component and provider reconciliation outcome

Metrics

  • Restaurant acceptance latency and timeout rate
  • Time to first offer, assignment latency, and unassigned delivery age by city
  • Courier location freshness and active delivery update lag
  • Late pickup/delivery rate and settlement exception count

Traces

  • Trace order placement through restaurant acceptance and dispatch
  • Trace completion through capture and participant settlement

Alerts

  • Growing accepted-but-unassigned delivery backlog
  • City-level location freshness collapses
  • Completed deliveries remain unsettled beyond the operating window

Testing strategy

LayerWhat it proves
UnitState machines, quote expiry, fee allocation, ETA fallback
IntegrationPostGIS service-area queries, assignment CAS, outbox, settlement constraints
ContractRestaurant adapter, payment webhooks, mobile realtime event versions
ConcurrencyHundreds of couriers accept one offer; exactly one assignment
FailureSilent restaurant, offline courier, duplicate completion, provider timeout
End-to-end/loadOrder through settlement; burst tests aligned to meal-time geographic hotspots

Do not mock PostgreSQL constraints, transaction isolation, object-store semantics, or provider contract fixtures in the tests intended to prove those boundaries.

Deployment evolution

Stage 1
Modular API, PostgreSQL/PostGIS, Redis, managed queue, WebSocket gateway, workers
Stage 2
Replica APIs, separate dispatch workers, partitioned realtime channels, read replica for restaurant browsing
Stage 3
City-owned dispatch partitions and regional event streams; settlement remains separately controlled

Scale evolution

These are architecture scenarios, not throughput claims. Each transition should follow measured workload and reliability pressure.

ScenarioArchitecture may evolve towardDecision rule
1K usersOne API, PostgreSQL with PostGIS for orders and delivery zones, a dispatch worker, a WebSocket gateway for order statusRun one dispatcher per city; courier locations may be dropped or stale, order state may not
100K usersScale reads, realtime gateways, and dispatch workers by city; isolate location writes from order PostgreSQLMove courier location writes off the order database once they compete with order transactions
1M+ usersAssign city/region ownership for dispatch and location streams; keep order/settlement invariants within a single writer boundaryGive each city one dispatch owner; cross-city work happens only in reporting and settlement batches