Session Recap — Food Delivery API Design¶
Starting Point¶
Third problem in the API + Application Design pillar. More complex than URL shortener and hotel booking — three distinct actor types, a multi-step state machine, spatial queries for driver matching, and payment as a cross-cutting concern.
What We Did¶
Step 1 — Domain, state machine, and actors¶
DOMAIN:
USER(id PK)
RESTAURANT(id PK, location GEOGRAPHY(POINT))
ORDER(id PK, restaurant_id FK, user_id FK, status)
ORDER_ITEM(id PK, order_id FK, menu_item_id FK, quantity)
MENU_ITEM(id PK, restaurant_id FK, name, price)
DRIVER(id PK, location GEOGRAPHY(POINT))
ORDER STATE MACHINE:
created → approved → prepared → enroute → delivered
created → cancelled (hard stop at created only, not after approval)
created → rejected
ACTORS:
USER → search restaurants, view restaurant, create order, cancel order, view own orders
RESTAURANT → view incoming orders, approve, reject, mark prepared
DRIVER → view nearby prepared orders, mark enroute, mark delivered
Step 2 — Endpoints¶
#USER
GET /restaurants?gps=&range=
GET /restaurants/{id}
POST /orders
GET /users/me/orders/{id}
POST /orders/{id}/cancel
#RESTAURANT
GET /restaurants/me/orders?status=created
POST /orders/{id}/approve
POST /orders/{id}/reject
POST /orders/{id}/prepared
#DRIVER
GET /driver/orders?status=prepared&gps=&range=
POST /orders/{id}/enroute
POST /orders/{id}/delivered
Driver-scoped namespace /driver/orders keeps driver-specific filtering logic out of the general orders endpoint. Different actor, different namespace.
Step 3 — POST /orders contract¶
Request:
{
"items": [
{ "menuItemId": "abc", "quantity": 2 }
],
"deliveryAddress": {
"name": "Abhishek",
"phone": "9101284041",
"gpsCoordinates": { "lat": 22.5, "lng": 88.3 }
}
}
Response (201):
{
"orderId": "xyz",
"items": [...],
"createdAt": "2026-05-02T10:00:00Z",
"status": "created",
"restaurantId": "abc",
"estimatedDeliveryTime": "2026-05-02T10:45:00Z",
"deliveryAddress": {
"name": "Abhishek",
"phone": "9101284041",
"gpsCoordinates": { "lat": 22.5, "lng": 88.3 }
}
}
user_id is NOT in the request body — server extracts it from JWT. Client sending their own userId is unnecessary surface area and a security risk.
estimatedDeliveryTime is in the response — first thing a customer wants to know after placing an order.
deliveryAddress echoed back — confirms server received it correctly.
Step 4 — Schema¶
USER(id PK)
RESTAURANT(id PK, location GEOGRAPHY(POINT))
MENU_ITEM(id PK, restaurant_id FK, name, price)
ORDER(id PK, restaurant_id FK, user_id FK, driver_id FK nullable, status, created_at)
ORDER_ITEM(id PK, order_id FK, menu_item_id FK, quantity)
DRIVER(id PK, location GEOGRAPHY(POINT))
Indexes:
RESTAURANT: GIST idx(location)
DRIVER: GIST idx(location)
ORDER: idx(restaurant_id, status), idx(user_id)
Concepts Covered¶
items is a relationship, not a column¶
Initial schema had items as a column on ORDER. That's wrong — items is a one-to-many relationship. Fix: ORDER_ITEM junction table with order_id and menu_item_id.
userId from JWT, not request body¶
Never put user_id in the POST body when the user is authenticated. Server extracts identity from JWT. Sending it in the body means the client could send someone else's ID — even with validation it's unnecessary risk.
State machines in API design¶
Define the valid state transitions explicitly before designing endpoints. Each transition that has cross-aggregate effects gets its own action endpoint. Invalid transitions return 422.
For this problem: cancel is only valid from created. Attempting cancel from approved or later returns 422. In a real app there'd be a short cancellation window after approval — a business rule, not a technical one.
Actor-scoped namespaces¶
/driver/orders instead of GET /orders?actorType=driver. The namespace:
- Makes routing intent explicit
- Applies different middleware per actor type
- Keeps actor-specific filtering logic out of shared handlers
- Self-documents who the endpoint is for
Same principle as /admin/... from hotel booking.
PATCH vs action endpoints (reinforced)¶
All order state transitions (approve, reject, prepared, enroute, delivered, cancel) are action endpoints — each triggers cross-aggregate side effects: notifications, driver assignment, payment, inventory. PATCH is wrong here.
estimatedDeliveryTime in POST response¶
Customer's first question after ordering is "when does it arrive." Return it immediately in the creation response — don't make them poll a separate endpoint.
Spatial queries (same as hotel booking)¶
GET /restaurants?gps=&range= and GET /driver/orders?status=prepared&gps=&range= both need spatial queries. Same PostGIS pattern: GEOGRAPHY(POINT) column, GIST index, ST_DWithin for radius filtering.
What We Messed Up¶
items as a column
First pass had ORDER(id, restaurant_id, items, status). items cannot be a column — it's a relationship to multiple menu items with quantities. Fix: ORDER_ITEM junction table.
Key Values and Config to Remember¶
| Item | Value |
|---|---|
| Order states | created, approved, rejected, prepared, enroute, delivered, cancelled |
| Cancel valid from | created only (hard stop) |
| Invalid state transition | 422 |
| Driver namespace | /driver/orders |
| userId in request body | Never — use JWT |
| Response must include | estimatedDeliveryTime, deliveryAddress echo |
| Spatial column | GEOGRAPHY(POINT) |
| Spatial index | GIST |
Unanswered Questions / Things to Investigate¶
- PostGIS in depth — needs dedicated session (same as hotel booking).
- Payment flow — not scoped. If added: idempotency key =
order-{orderId}for the payment request. See Concepts recap. - Driver assignment — how does a driver get assigned to an order? Not designed. Options: driver self-assigns via
POST /orders/{id}/enroute, or dispatcher assigns via admin endpoint. GET /restaurantspagination — not designed. Apply keyset pagination for large result sets.GET /restaurants/me/orders— restaurant polling for new orders. In a real system this would be push (websocket or SSE), not pull. Not scoped here.
What's Next¶
Food Delivery is complete. See Concepts recap for pagination, idempotency, error contracts, versioning, and rate limiting — all of which apply directly to this problem.