Estimate the scale of the system. Consider daily active users, read/write ratio, storage requirements, bandwidth, and any relevant QPS calculations...
Define the APIs expected from the system. This is your chance to analyze and define the read and write paths so that you can come up with the high-level design...
OST /v1/orders
json
{
"restaurantId": "rest_abc",
"items": [
{"menuItemId": "item_1", "quantity": 2, "customizations": ["extra cheese"]},
{"menuItemId": "item_2", "quantity": 1, "customizations": []}
],
"deliveryAddress": {"lat": 37.7749, "lng": -122.4194, "formatted": "123 Main St"},
"paymentMethodId": "pm_xyz",
"idempotencyKey": "ord_client_abc123",
"scheduledFor": null,
"tip": 500
}
Returns 201 with order ID, estimated delivery time, and order total. The idempotencyKey is client-generated, if the client retries (network timeout), the server detects the duplicate key and returns the existing order instead of creating a new one.
GET /v1/orders/{orderId}. Full order details including items, status, driver info, and status history.
POST /v1/orders/{orderId}/cancel. Cancel with a required reason field. Only allowed in states: placed, confirmed. Returns 409 if the order is already being prepared.
GET /v1/restaurants?lat={lat}&lng={lng}&radius=5km&cuisine=italian&sort=rating
Returns paginated list of nearby open restaurants with name, cuisine, rating, estimated delivery time, and delivery fee. Results filtered by operating hours (don't show closed restaurants) and the user's delivery radius.
GET /v1/restaurants/{restaurantId}/menu. Full menu grouped by category (appetizers, mains, drinks) with item names, descriptions, photos, prices, and availability flags.
PUT /v1/restaurants/{restaurantId}/menu/{itemId}. Update item details or mark as sold out. Setting available: false immediately hides the item from users browsing the menu.
POST /v1/driver/location
json
{"lat": 37.7750, "lng": -122.4195, "heading": 90, "speed": 25, "timestamp": "2026-03-01T18:30:00Z"}
Called every 5 seconds by the driver app. Writes to Redis with geohash index. Minimal payload for bandwidth efficiency on cellular connections.
POST /v1/orders/{orderId}/accept. Driver accepts a delivery offer. Uses optimistic locking (the offer includes a version token), if another driver already accepted (shouldn't happen with sequential offers), returns 409.
POST /v1/orders/{orderId}/status. Driver updates order status: picked_up, arrived, delivered. Each transition is validated against the state machine.
GET /v1/driver/earnings?from=2026-02-01&to=2026-02-28. Returns the driver's earnings breakdown: total earnings, number of deliveries, tips, and bonuses for the specified period. Used by the driver app's earnings dashboard.
WebSocket /v1/tracking/{orderId}. Client connects after order placement. Server pushes: status changes (preparing → ready → picked up → delivered), driver location updates (every 5 seconds when en route), and ETA updates. The connection is authenticated via a short-lived token in the handshake.
Describe the overall system architecture. Identify the main components needed to solve the problem end-to-end. Use the diagramming tool to create a block diagram.
A food delivery service connects hungry users, restaurants, and delivery drivers through a real-time marketplace. Think DoorDash, Uber Eats, or Grubhub. The platform coordinates three distinct user types with very different needs: customers ordering food, restaurants preparing it, and drivers delivering it.
POST /v1/orders
json
{
"restaurantId": "rest_abc",
"items": [
{"menuItemId": "item_1", "quantity": 2, "customizations": ["extra cheese"]},
{"menuItemId": "item_2", "quantity": 1, "customizations": []}
],
"deliveryAddress": {"lat": 37.7749, "lng": -122.4194, "formatted": "123 Main St"},
"paymentMethodId": "pm_xyz",
"idempotencyKey": "ord_client_abc123",
"scheduledFor": null,
"tip": 500
}
Returns 201 with order ID, estimated delivery time, and order total. The idempotencyKey is client-generated, if the client retries (network timeout), the server detects the duplicate key and returns the existing order instead of creating a new one.
GET /v1/orders/{orderId}. Full order details including items, status, driver info, and status history.
POST /v1/orders/{orderId}/cancel. Cancel with a required reason field. Only allowed in states: placed, confirmed. Returns 409 if the order is already being prepared.
GET /v1/restaurants?lat={lat}&lng={lng}&radius=5km&cuisine=italian&sort=rating
Returns paginated list of nearby open restaurants with name, cuisine, rating, estimated delivery time, and delivery fee. Results filtered by operating hours (don't show closed restaurants) and the user's delivery radius.
GET /v1/restaurants/{restaurantId}/menu. Full menu grouped by category (appetizers, mains, drinks) with item names, descriptions, photos, prices, and availability flags.
PUT /v1/restaurants/{restaurantId}/menu/{itemId}. Update item details or mark as sold out. Setting available: false immediately hides the item from users browsing the menu.
POST /v1/driver/location
json
{"lat": 37.7750, "lng": -122.4195, "heading": 90, "speed": 25, "timestamp": "2026-03-01T18:30:00Z"}
Called every 5 seconds by the driver app. Writes to Redis with geohash index. Minimal payload for bandwidth efficiency on cellular connections.
POST /v1/orders/{orderId}/accept. Driver accepts a delivery offer. Uses optimistic locking (the offer includes a version token), if another driver already accepted (shouldn't happen with sequential offers), returns 409.
POST /v1/orders/{orderId}/status. Driver updates order status: picked_up, arrived, delivered. Each transition is validated against the state machine.
GET /v1/driver/earnings?from=2026-02-01&to=2026-02-28. Returns the driver's earnings breakdown: total earnings, number of deliveries, tips, and bonuses for the specified period. Used by the driver app's earnings dashboard.
WebSocket /v1/tracking/{orderId}. Client connects after order placement. Server pushes: status changes (preparing → ready → picked up → delivered), driver location updates (every 5 seconds when en route), and ETA updates. The connection is authenticated via a short-lived token in the handshake.
| Table | Key Columns | Notes |
| users | user_id (PK), email (UNIQUE), name, phone, default_address (JSONB) | Customer accounts |
| restaurants | restaurant_id (PK), name, location (POINT), cuisine_type, rating, operating_hours (JSONB), is_active | PostGIS for geo queries |
| menu_items | item_id (PK), restaurant_id (FK), name, price_cents, category, is_available, customizations (JSONB) | Per-restaurant menus |
| orders | order_id (PK), user_id (FK), restaurant_id (FK), driver_id (FK), status, items (JSONB), total_cents, delivery_address (JSONB), idempotency_key (UNIQUE) | Items stored as point-in-time snapshot; idempotency_key prevents duplicate orders |
| order_status_history | id (PK), order_id (FK), status, changed_by, changed_at | Audit trail, who changed status and when |
| drivers | driver_id (PK), name, phone, vehicle_type, is_available, rating | Current location stored in Redis, not here |
| payments | payment_id (PK), order_id (FK), amount_cents, method, status, provider_txn_id | Status: authorized → captured → refunded |
PostgreSQL for orders, users, restaurants, menu items, payments, relational data with ACID requirements. Order placement is a transaction across orders, order_status_history, and payments tables. PostGIS extension enables geospatial queries: SELECT * FROM restaurants WHERE ST_DWithin(location, ST_MakePoint(lng, lat), 5000) finds restaurants within 5km. Read replicas handle the 10K req/sec read traffic during peak hours.
Redis for real-time driver locations, each driver's current position stored as a Redis geospatial entry: GEOADD drivers:locations lng lat driver_id. Finding nearest drivers: GEORADIUS drivers:locations lng lat 5 km COUNT 10 ASC. Sub-millisecond reads. Also stores: idempotency keys (SET with TTL), rate limiting counters, and cached restaurant menus (cache-aside pattern: on menu request, check Redis first; on cache miss, read from PostgreSQL, populate Redis with a 5-minute TTL, and return. When a restaurant updates their menu, invalidate the cache key so the next request fetches fresh data).
Kafka for event streaming, order state transitions, driver location traces, payment events, and notification triggers all flow through Kafka topics. Decouples producers from consumers. Enables replay for recovery.
S3 + CDN for images, restaurant photos, food item images. Immutable once uploaded. CDN cache hit rate ~99% for food photos (they rarely change).
orders(user_id, created_at DESC), user's order historyorders(restaurant_id, status), restaurant's pending ordersorders(driver_id, status), driver's active deliveriesorders(idempotency_key), unique index for duplicate preventionrestaurants(location). GiST index for geospatial proximity searchmenu_items(restaurant_id, is_available), available items per restaurantFood delivery platform: clients, API gateway, microservices, and data stores
Define the data model. Identify the main entities, their attributes, and relationships. Consider the choice of database type (SQL vs NoSQL) and justify your decision based on access patterns...
| Table | Key Columns | Notes |
| users | user_id (PK), email (UNIQUE), name, phone, default_address (JSONB) | Customer accounts |
| restaurants | restaurant_id (PK), name, location (POINT), cuisine_type, rating, operating_hours (JSONB), is_active | PostGIS for geo queries |
| menu_items | item_id (PK), restaurant_id (FK), name, price_cents, category, is_available, customizations (JSONB) | Per-restaurant menus |
| orders | order_id (PK), user_id (FK), restaurant_id (FK), driver_id (FK), status, items (JSONB), total_cents, delivery_address (JSONB), idempotency_key (UNIQUE) | Items stored as point-in-time snapshot; idempotency_key prevents duplicate orders |
| order_status_history | id (PK), order_id (FK), status, changed_by, changed_at | Audit trail, who changed status and when |
| drivers | driver_id (PK), name, phone, vehicle_type, is_available, rating | Current location stored in Redis, not here |
| payments | payment_id (PK), order_id (FK), amount_cents, method, status, provider_txn_id | Status: authorized → captured → refunded |
PostgreSQL for orders, users, restaurants, menu items, payments, relational data with ACID requirements. Order placement is a transaction across orders, order_status_history, and payments tables. PostGIS extension enables geospatial queries: SELECT * FROM restaurants WHERE ST_DWithin(location, ST_MakePoint(lng, lat), 5000) finds restaurants within 5km. Read replicas handle the 10K req/sec read traffic during peak hours.
Redis for real-time driver locations, each driver's current position stored as a Redis geospatial entry: GEOADD drivers:locations lng lat driver_id. Finding nearest drivers: GEORADIUS drivers:locations lng lat 5 km COUNT 10 ASC. Sub-millisecond reads. Also stores: idempotency keys (SET with TTL), rate limiting counters, and cached restaurant menus (cache-aside pattern: on menu request, check Redis first; on cache miss, read from PostgreSQL, populate Redis with a 5-minute TTL, and return. When a restaurant updates their menu, invalidate the cache key so the next request fetches fresh data).
Kafka for event streaming, order state transitions, driver location traces, payment events, and notification triggers all flow through Kafka topics. Decouples producers from consumers. Enables replay for recovery.
S3 + CDN for images, restaurant photos, food item images. Immutable once uploaded. CDN cache hit rate ~99% for food photos (they rarely change).
Deep dive into 2-3 key components. Explain how they work, how they scale, discuss tradeoffs, capacity, and any relevant algorithms or data structures.
This is the heart of the food delivery system. Every order moves through a finite set of states with strictly enforced transitions. Invalid transitions are rejected, preventing corrupted order states.
States: placed → confirmed → preparing → ready_for_pickup → picked_up → out_for_delivery → delivered. Branch states: cancelled (from placed or confirmed), refunded (from cancelled after payment was captured).
State transitions are atomic. Each transition is a PostgreSQL transaction that:
SELECT ... FOR UPDATE (row-level lock).orders.status.order_status_history.Why SELECT FOR UPDATE? Multiple actors can trigger state changes simultaneously: the restaurant sets "preparing" while the user cancels. Without row-level locking, both transactions could read status = "confirmed" and both succeed, the order ends up as both "preparing" and "cancelled." SELECT FOR UPDATE serializes these: the first transaction locks the row, the second waits. When the lock releases, the second transaction reads the updated status and either proceeds or rejects.
Idempotency keys for order placement. The client generates a UUID idempotency key per order attempt. The server checks Redis first (fast path, O(1)). If not found, proceeds to create the order. The database UNIQUE constraint on idempotency_key is the safety net for the race condition where Redis check passes for two simultaneous retries. The first INSERT succeeds; the second gets a constraint violation, which the application catches and returns the existing order.
Timeout handling. If a restaurant doesn't accept an order within 5 minutes, the system auto-cancels and releases the payment hold. A scheduled job (Kafka delayed message or Redis sorted set with timestamp scores) monitors orders in "placed" state and triggers timeouts. This prevents orders from being stuck indefinitely.
Key Insight
The order state machine uses SELECT FOR UPDATE (pessimistic locking) rather than optimistic concurrency because state transitions are irreversible decisions, not retryable operations. When a restaurant marks an order as preparing while the user simultaneously cancels, the loser must see the current state and get an immediate rejection, not a retry loop.
Geohash-based driver matching: GPS to geohash encoding, Redis geospatial queries, and proximity search
Geohashing encodes latitude/longitude into a compact string where nearby locations share a common prefix. geohash("37.7749, -122.4194") → "9q8yyk". Locations within ~600m share the first 6 characters. This turns 2D proximity search into a 1D prefix search.
Redis GEORADIUS uses a sorted set with geohash-encoded scores. GEORADIUS drivers:locations -122.4194 37.7749 3 km COUNT 10 ASC returns the 10 nearest drivers within 3km, sorted by distance. This is O(N+log(M)) where N is the number of items in the radius and M is the total number of items in the sorted set, fast for typical driver densities (50-200 drivers within 3km in a metro area).
Noisy GPS data. Urban canyons (tall buildings), tunnels, and poor cellular signal cause GPS readings to jump erratically, a driver stationary in traffic might appear to teleport 200 meters. The Location Service applies a Kalman filter before writing to Redis: it models the driver's position as a state with velocity, and new GPS readings are weighted against the predicted position. Sudden jumps are dampened. The filtered position is what users see on the map.
Why geohash over quadtree? Both encode 2D space for proximity search, but they make different trade-offs. A quadtree recursively subdivides space into four quadrants, adapting cell size to point density, downtown Manhattan (500 drivers/km²) gets small cells while rural Kansas (1 driver/km²) gets large ones. This gives optimal query performance for non-uniform distributions. However, a quadtree is an in-memory tree structure that doesn't map to Redis or any standard key-value store; you'd need a custom service to hold and query it. Geohashing encodes coordinates into a string where nearby points share prefixes. It maps directly to Redis sorted sets, GEORADIUS is a single built-in command, no custom data structures needed. The downside: geohash cells are fixed-size, so a dense downtown area and an empty suburb have the same cell granularity. For driver matching, this is acceptable because:
Common Pitfall
Do not match drivers purely by Euclidean distance. A driver 2km away heading toward the restaurant arrives faster than a driver 1.5km away heading in the opposite direction. The effective distance formula must account for heading and speed, or your ETA predictions will be consistently wrong.
Driver matching optimization. Finding the nearest driver isn't always optimal. A driver 2km away but heading toward the restaurant (based on heading) is better than a driver 1.5km away heading away. The Delivery Service computes an adjusted distance: effective_distance = actual_distance + speed * (1 - cos(angle_to_restaurant)) * time_factor. This penalizes drivers heading in the wrong direction.
Auto-scaling during dinner rush: metrics, scaling decisions, and graceful degradation
Predictable peaks (daily lunch/dinner): Pre-scale based on historical traffic patterns. By 11 AM, auto-scaler has already provisioned extra Order Service and Delivery Service instances for the lunch rush. This avoids the cold-start delay of reactive scaling.
Unpredictable spikes (weather, events): Reactive auto-scaling on leading indicators: Kafka consumer lag (messages piling up = consumers can't keep up), API P99 latency (rising = overloaded), and Redis command queue depth. Scale-up trigger: any metric exceeds threshold for 60 seconds. Scale-up action: add 50% more instances (aggressive, better to overshoot than undershoot during a spike).
Graceful degradation: When scaling isn't enough, shed non-critical load: