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
| Concern | Decision |
|---|---|
| Availability | Browse may serve slightly stale data; accepting orders and assignments fail closed when ownership cannot be established. |
| Latency | Dispatch candidates and location fan-out are latency-sensitive; settlement is asynchronous. |
| Consistency | Order state and courier assignment require conditional transitions; map position and ETA are eventual. |
| Privacy | Precise courier/customer locations are retained briefly and exposed only during an active delivery. |
| Auditability | Every manual reassignment, refund, and state override records actor and reason. |
User roles
| Role | Access boundary |
|---|---|
| Customer | Own orders and active courier tracking |
| Restaurant staff | Only their restaurant's menu and incoming orders |
| Courier | Own offers, active job, and minimum pickup/drop-off details |
| Dispatcher/support | Region-scoped operations; sensitive actions audited |
Core workflows
Domain model
| Entities | Relationship |
|---|---|
Restaurant → Menu → MenuItem | A published menu version is priced at order time |
Order → OrderLine | Order stores immutable names, modifiers, taxes, and prices |
Order → Delivery | Accepted order creates one delivery state machine |
Delivery → CourierOffer | Many offers may exist; at most one can win |
Courier → LocationSample | Ephemeral 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
orders — Commercial and kitchen workflow
deliveries — Pickup/drop-off state and winner
courier_offers — Expiring dispatch proposals
ledger_entries — Immutable order settlement components
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
| Endpoint | Purpose and permission | Idempotency / status |
|---|---|---|
GET /v1/restaurants?lat=…&lng=…&cursor=… | Discover serviceable open restaurants Public with rate limits | Not required 200, 400 |
POST /v1/orders/quote | Return expiring server-calculated quote Customer | Not required 200, 409, 422 |
POST /v1/orders | Authorize and place order Customer | Required 201, 202, 402, 409 |
POST /v1/restaurant-orders/:id/accept | Accept with promised preparation time Restaurant staff for owning restaurant | State transition is idempotent 200, 409 |
POST /v1/courier-offers/:id/accept | Attempt to win delivery assignment Offer recipient | Required; atomic winner check 200, 409, 410 |
PATCH /v1/deliveries/:id/status | Advance 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
Main application architecture
Critical workflow sequence
Event model
| Event | Producer → consumers | Delivery |
|---|---|---|
RestaurantAcceptedOrder | Order service → Dispatch, notifications | Outbox; duplicate-safe dispatch creation |
CourierAssigned | Dispatch → Order projection, realtime gateway | At-least-once with delivery version |
DeliveryCompleted | Delivery → Settlement, receipts, analytics | Settlement 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
| Layer | What it proves |
|---|---|
| Unit | State machines, quote expiry, fee allocation, ETA fallback |
| Integration | PostGIS service-area queries, assignment CAS, outbox, settlement constraints |
| Contract | Restaurant adapter, payment webhooks, mobile realtime event versions |
| Concurrency | Hundreds of couriers accept one offer; exactly one assignment |
| Failure | Silent restaurant, offline courier, duplicate completion, provider timeout |
| End-to-end/load | Order 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
Scale evolution
These are architecture scenarios, not throughput claims. Each transition should follow measured workload and reliability pressure.
| Scenario | Architecture may evolve toward | Decision rule |
|---|---|---|
| 1K users | One API, PostgreSQL with PostGIS for orders and delivery zones, a dispatch worker, a WebSocket gateway for order status | Run one dispatcher per city; courier locations may be dropped or stale, order state may not |
| 100K users | Scale reads, realtime gateways, and dispatch workers by city; isolate location writes from order PostgreSQL | Move courier location writes off the order database once they compete with order transactions |
| 1M+ users | Assign city/region ownership for dispatch and location streams; keep order/settlement invariants within a single writer boundary | Give each city one dispatch owner; cross-city work happens only in reporting and settlement batches |