Build a platform like Uber Eats, DoorDash, Swiggy, or Zomato from scratch
Master restaurant discovery, real-time tracking, delivery partner matching, ETA prediction, and handling millions of orders during peak hours
A Food Delivery Platform is a three-sided marketplace connecting customers, restaurants, and delivery partners. Unlike traditional e-commerce, it involves perishable goods, time-sensitive operations, and complex real-time coordination between multiple parties.
Customers
Browse restaurants, place orders, track delivery, rate experience
Restaurants
Manage menu, accept orders, prepare food, update status
Delivery Partners
Accept deliveries, pick up food, navigate to customer, complete delivery
Let's estimate the scale for a platform operating in a major metro city.
2M orders / 86,400 sec ≈ 23 orders/sec
1.2M orders / 6 hours / 3600 sec ≈ 55 orders/sec
100,000 / 5 = 20,000 location updates/sec
10x order rate ≈ 550 searches/sec during peak
Key Insight: The system has two critical real-time components: location tracking (20K updates/sec) and order state changes. Both require low-latency infrastructure and careful event processing.
┌─────────────────────────────────────────────────────────────────────────────┐
│ CLIENT APPS │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Customer App │ │Restaurant App│ │ Driver App │ │
│ │ (iOS/Web) │ │ (Tablet) │ │ (Mobile) │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
│
┌────────────┴────────────┐
│ API Gateway │
│ (Rate Limit, Auth) │
└────────────┬────────────┘
│
┌─────────────────────────────────────────────────────────────────────────────┐
│ CORE SERVICES │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
│ │ Restaurant │ │ Order │ │ Delivery │ │ Search │ │
│ │ Service │ │ Service │ │ Service │ │ Service │ │
│ └────────────┘ └────────────┘ └────────────┘ └────────────┘ │
│ │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
│ │ Location │ │ ETA │ │ Payment │ │ User │ │
│ │ Service │ │ Service │ │ Service │ │ Service │ │
│ └────────────┘ └────────────┘ └────────────┘ └────────────┘ │
│ │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
│ │Notification│ │ Rating │ │ Pricing │ │ Promo │ │
│ │ Service │ │ Service │ │ Service │ │ Service │ │
│ └────────────┘ └────────────┘ └────────────┘ └────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────────────────────────┐
│ DATA & MESSAGING │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ PostgreSQL │ │ Redis │ │Elasticsearch │ │ Kafka │ │
│ │ (Orders) │ │ (Location) │ │ (Search) │ │ (Events) │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ MongoDB │ │ TimeSeries │ │ S3/CDN │ │
│ │ (Menus) │ │ (ETA) │ │ (Images) │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
Users need to quickly find restaurants based on location, cuisine, ratings, and availability.
Geohash converts latitude/longitude into a string. Nearby locations share the same prefix, enabling efficient range queries.
Location: 40.7128° N, 74.0060° W (New York)
Geohash: dr5ru7 (precision 6 = ~1.2km × 0.6km cell)
Nearby Search: Find all restaurants where geohash LIKE 'dr5ru%'
This is much faster than calculating distance for every restaurant!
-- Elasticsearch geo_distance query
{
"query": {
"bool": {
"must": { "match": { "cuisine": "italian" } },
"filter": {
"geo_distance": {
"distance": "5km",
"location": { "lat": 40.7128, "lon": -74.0060 }
}
}
}
},
"sort": [
{ "_geo_distance": { "location": [40.7128, -74.0060], "order": "asc" } },
{ "rating": "desc" }
]
}Pro Tip: Pre-compute restaurant rankings for each geohash cell during off-peak hours. Serve from cache during peak. Update cache every 5 minutes.
CART │ ▼ [Checkout initiated] PENDING_PAYMENT │ ├──▶ [Payment failed] ──▶ PAYMENT_FAILED │ ▼ [Payment success] PLACED │ ├──▶ [Restaurant rejects] ──▶ REJECTED (refund triggered) │ ▼ [Restaurant accepts] CONFIRMED │ ▼ [Food being prepared] PREPARING │ ▼ [Food ready, driver assigned] READY_FOR_PICKUP │ ▼ [Driver picks up] PICKED_UP │ ▼ [Driver en route] OUT_FOR_DELIVERY │ ├──▶ [Customer unavailable] ──▶ DELIVERY_FAILED │ ▼ [Delivered successfully] DELIVERED │ ▼ [After 24 hours] COMPLETED (ratings enabled)
Customer Places Order
Validate cart items, calculate pricing with taxes & delivery fee, initiate payment
Payment Processing
Async payment via Stripe/Razorpay. Order stays in PENDING_PAYMENT until confirmed.
Restaurant Notification
Push notification to restaurant app. They have 60 seconds to accept or auto-reject.
Driver Assignment (Parallel)
Start looking for drivers immediately. Don't wait for restaurant to accept.
Preparation Tracking
Restaurant updates prep status. Driver arrives just as food is ready.
// Kafka Topics order.placed → Triggers: payment processing, restaurant notification order.confirmed → Triggers: driver assignment, ETA calculation order.preparing → Triggers: customer notification, driver dispatch timing order.ready → Triggers: driver notification to head to restaurant order.picked-up → Triggers: customer notification, live tracking enabled order.delivered → Triggers: payment settlement, rating prompt // Consumer Groups - payment-processor (order.placed) - restaurant-notifier (order.placed) - delivery-matcher (order.confirmed) - eta-calculator (order.confirmed, order.picked-up) - notification-sender (all events) - analytics-collector (all events)
Finding the optimal driver is critical for delivery time and driver satisfaction.
function findOptimalDriver(order, restaurant) {
// Step 1: Get nearby available drivers
const nearbyDrivers = await redis.georadius(
'driver_locations',
restaurant.longitude,
restaurant.latitude,
3000, // 3km radius
'WITHDIST'
);
// Step 2: Filter by status
const availableDrivers = nearbyDrivers.filter(d =>
d.status === 'ONLINE' &&
d.currentOrders < d.maxConcurrentOrders &&
!d.isOnBreak
);
// Step 3: Score each driver
const scoredDrivers = availableDrivers.map(driver => ({
driver,
score: calculateDriverScore(driver, order, restaurant)
}));
// Step 4: Sort and pick top driver
scoredDrivers.sort((a, b) => b.score - a.score);
// Step 5: Send offer to top driver
const topDriver = scoredDrivers[0];
await sendDeliveryOffer(topDriver.driver, order, timeout: 30);
// If rejected/timeout, try next driver
}
function calculateDriverScore(driver, order, restaurant) {
let score = 0;
// Distance to restaurant (35%)
const distToRestaurant = driver.distanceMeters;
score += (1 - distToRestaurant / 3000) * 35;
// Driver rating (20%)
score += (driver.rating / 5) * 20;
// Acceptance rate (15%)
score += driver.acceptanceRate * 15;
// Time since last delivery (15%) - avoid idle drivers
const idleMinutes = (Date.now() - driver.lastDeliveryAt) / 60000;
score += Math.min(idleMinutes / 30, 1) * 15;
// Vehicle type match (15%)
if (order.requiresLargeVehicle && driver.vehicleType === 'CAR') {
score += 15;
} else if (!order.requiresLargeVehicle) {
score += 15; // Bike is fine
}
return score;
}One driver can pick up from multiple restaurants if they're close and going in the same direction.
If assigned driver becomes unavailable, quickly reassign without impacting delivery time.
Handling 20,000+ location updates per second requires specialized infrastructure.
Driver App Location Service Redis Geo Customer App
│ │ │ │
│ POST /location │ │ │
│ {lat, lng, timestamp} │ │ │
│ ──────────────────────────▶│ │ │
│ │ │ │
│ │ GEOADD driver_locations │ │
│ │ driver_123 lng lat │ │
│ │ ─────────────────────────▶│ │
│ │ │ │
│ │ PUBLISH driver:123 │ │
│ │ {lat, lng, heading} │ │
│ │ ─────────────────────────▶│ │
│ │ │ │
│ │ │ WebSocket push │
│ │ │ ──────────────────▶│
│ │ │ │
│ │ │ Update map marker │
│ │ │ │
// Redis commands for location
GEOADD driver_locations -73.935242 40.730610 driver_123
GEORADIUS driver_locations -73.935 40.730 3 km WITHDIST WITHCOORD
// Pub/Sub for live tracking
PUBLISH order:456:location '{"lat":40.730,"lng":-73.935,"eta":5}'
GPS can be jumpy. Smooth the marker movement on the customer app.
// Animate marker between updates animateMarker( currentPos, newPos, duration: 3000ms, easing: 'linear' );
Accurate ETAs build customer trust. An ML model predicts delivery time based on multiple factors.
Total ETA = Restaurant Accept Time
+ Food Preparation Time
+ Driver Arrival at Restaurant
+ Pickup Time
+ Travel Time to Customer
+ Handoff Time
// Each component predicted separately
{
"restaurant_accept": "2 min (avg for this restaurant)",
"preparation": "18 min (based on order items + current load)",
"driver_to_restaurant": "5 min (real-time traffic)",
"pickup": "3 min (avg for this restaurant)",
"delivery_travel": "8 min (Google Maps API + ML adjustment)",
"handoff": "2 min (building type factor)",
"total_eta": "38 min",
"confidence": 0.85
}Time-based
Hour of day, day of week, holidays, weather
Restaurant-based
Historical prep time, current queue, cuisine type
Location-based
Traffic patterns, distance, building access difficulty
Key Insight: Update ETA dynamically as order progresses. A stale ETA (still showing 30 min when food is ready) frustrates customers. Push new ETA on every state change.
During lunch (12-2 PM) and dinner (7-10 PM), order volume spikes 3-5x. Additionally, cricket match finals or New Year's Eve can cause 10x spikes. The system must handle this gracefully.
When demand exceeds supply, increase delivery fee to balance the market.
demandSupplyRatio = pendingOrders / availableDrivers if (ratio > 1.5) surgeFee = baseFee * 1.3 // 30% surge if (ratio > 2.0) surgeFee = baseFee * 1.5 // 50% surge if (ratio > 3.0) surgeFee = baseFee * 2.0 // 100% surge // Show surge warning before checkout "Delivery fee is higher than usual due to high demand"
Don't promise 30 min if you can't deliver. Show realistic ETAs.
// During peak hours baseETA = calculateBaseETA(order); peakMultiplier = getPeakMultiplier(currentHour, dayOfWeek); adjustedETA = baseETA * peakMultiplier; // Show on UI: "Delivery in 45-55 min (Busy time)" // Customer can decide if they want to wait or cook at home
Notify offline drivers about surge zones to increase supply.
# Pre-scheduled scaling for known peak hours 0 11 * * * kubectl scale deployment order-service --replicas=20 0 14 * * * kubectl scale deployment order-service --replicas=10 0 18 * * * kubectl scale deployment order-service --replicas=25 0 22 * * * kubectl scale deployment order-service --replicas=10 # Event-driven scaling for sudden spikes HPA with custom metrics: - orders_per_second > 50 → scale up - orders_per_second < 20 → scale down (with delay)
-- Restaurants
CREATE TABLE restaurants (
restaurant_id UUID PRIMARY KEY,
name VARCHAR(200),
location GEOGRAPHY(Point, 4326),
city_id VARCHAR(10),
cuisine_types TEXT[],
rating DECIMAL(2,1),
is_open BOOLEAN,
avg_prep_time INT, -- minutes
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_restaurant_geo ON restaurants USING GIST(location);
-- Menu Items
CREATE TABLE menu_items (
item_id UUID PRIMARY KEY,
restaurant_id UUID REFERENCES restaurants(restaurant_id),
name VARCHAR(200),
description TEXT,
price DECIMAL(10,2),
category VARCHAR(50),
is_available BOOLEAN DEFAULT true,
prep_time_mins INT
);
-- Orders (Sharded by city_id)
CREATE TABLE orders (
order_id UUID PRIMARY KEY,
customer_id UUID,
restaurant_id UUID,
driver_id UUID,
city_id VARCHAR(10),
status VARCHAR(30),
total_amount DECIMAL(10,2),
delivery_fee DECIMAL(10,2),
delivery_address JSONB,
delivery_location GEOGRAPHY(Point, 4326),
placed_at TIMESTAMP,
delivered_at TIMESTAMP,
eta_minutes INT
);
-- Delivery Partners
CREATE TABLE delivery_partners (
driver_id UUID PRIMARY KEY,
name VARCHAR(100),
phone VARCHAR(20),
vehicle_type VARCHAR(20),
rating DECIMAL(2,1),
status VARCHAR(20), -- ONLINE, OFFLINE, ON_DELIVERY
current_location GEOGRAPHY(Point, 4326),
city_id VARCHAR(10),
last_active TIMESTAMP
);// Notification Service
async function sendNotification(userId, event, data) {
const user = await getUser(userId);
const message = formatMessage(event, data);
// 1. In-app (always)
await pushToWebSocket(userId, message);
// 2. Push notification (if app in background)
if (!user.isAppActive) {
await sendPushNotification(user.fcmToken, message);
}
// 3. SMS (for critical events only)
if (event.isCritical && user.smsEnabled) {
await sendSMS(user.phone, message.shortText);
}
// 4. Email (for receipts, not time-sensitive)
if (event === 'ORDER_DELIVERED') {
await queueEmail(user.email, 'receipt', data);
}
}Practice with an AI interviewer and get instant feedback on your system design skills.