Build a 10-minute delivery system like Zepto, Blinkit, or Gopuff from scratch
Master dark store operations, real-time inventory, hyperlocal delivery, and handling millions of orders during flash sales
Quick Commerce (Q-Commerce) revolutionizes traditional e-commerce by promising 10-15 minute delivery of groceries and essentials. Unlike traditional e-commerce that relies on large warehouses, Q-Commerce uses a network of Dark Stores (micro-fulfillment centers) strategically placed within 1-3 km of customer clusters.
A Dark Store is a retail outlet or small warehouse that caters exclusively to online shopping. Unlike traditional stores, customers cannot walk in and browse. These stores are optimized for quick picking and packing operations.
Small Footprint
2,000-5,000 sq ft per store
Limited SKUs
5,000-10,000 high-velocity items
Hyperlocal
1-3 km delivery radius
Understanding the scale helps us make informed architectural decisions.
500K orders / 24 hours ≈ 5.8 orders/sec
250K orders / 3 hours / 3600 sec ≈ 23 orders/sec
Peak × 10 ≈ 230+ orders/sec
100:1 read ratio ≈ 23,000 reads/sec during peak
Key Insight: The system is extremely read-heavy but writes (inventory updates, order creation) are time-critical. During flash sales, both read and write spikes happen simultaneously - this is the hardest scenario to handle.
The system consists of multiple microservices working together to enable 10-minute delivery.
┌─────────────────────────────────────────────────────────────────────────────┐
│ CUSTOMER LAYER │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Mobile App │ │ Web App │ │ API Gateway │ │ CDN │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ SERVICE MESH (Istio) │
├─────────────────────────────────────────────────────────────────────────────┤
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
│ │ Catalog │ │ Inventory │ │ Order │ │ Pricing │ │
│ │ Service │ │ Service │ │ Service │ │ Service │ │
│ └────────────┘ └────────────┘ └────────────┘ └────────────┘ │
│ │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
│ │ Dark Store │ │ Delivery │ │ Search │ │ Payment │ │
│ │ Service │ │ Service │ │ Service │ │ Service │ │
│ └────────────┘ └────────────┘ └────────────┘ └────────────┘ │
│ │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
│ │ Notification│ │ Analytics │ │ Forecast │ │ User │ │
│ │ Service │ │ Service │ │ Service │ │ Service │ │
│ └────────────┘ └────────────┘ └────────────┘ └────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ DATA LAYER │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Postgres │ │ Redis │ │ Elasticsearch│ │ Kafka │ │
│ │ (Primary) │ │ (Cache) │ │ (Search) │ │ (Events) │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
Inventory accuracy is the backbone of quick commerce. A customer should never be able to order something that's out of stock.
Physical Stock = Available + Reserved + Damaged + In-Transit Where: ├── Available → Can be sold to customers ├── Reserved → Blocked for pending orders (soft lock) ├── Damaged → Cannot be sold └── In-Transit → Being moved between warehouse and dark store
When a customer adds items to cart and proceeds to checkout, we must reserve inventory to prevent overselling.
Cart Addition (Soft Reserve)
When item added to cart, create a soft reservation with 10-minute TTL. Decrement available count, increment reserved count.
Checkout (Hard Reserve)
Convert soft reservation to hard reservation. Start payment processing.
Payment Success (Commit)
Deduct from reserved count. Order moves to picking queue.
Payment Failure / TTL Expired (Rollback)
Release reservation. Add back to available count.
-- Atomic inventory reservation using Lua script
local available = redis.call('HGET', KEYS[1], 'available')
local requested = tonumber(ARGV[1])
if tonumber(available) >= requested then
redis.call('HINCRBY', KEYS[1], 'available', -requested)
redis.call('HINCRBY', KEYS[1], 'reserved', requested)
-- Set TTL for soft reservation
redis.call('SETEX', KEYS[2], 600, ARGV[2]) -- 10 min TTL
return 1 -- Success
else
return 0 -- Insufficient stock
end
-- Key structure: inventory:{store_id}:{sku_id}
-- KEYS[1] = inventory:DS001:SKU123
-- KEYS[2] = reservation:ORDER123:SKU123
-- ARGV[1] = quantity
-- ARGV[2] = order_idWhy Lua Script? Redis Lua scripts are atomic - no race conditions. Multiple concurrent checkout requests cannot oversell inventory.
-- Store-level inventory (source of truth)
CREATE TABLE store_inventory (
store_id VARCHAR(20),
sku_id VARCHAR(50),
available_qty INT NOT NULL DEFAULT 0,
reserved_qty INT NOT NULL DEFAULT 0,
damaged_qty INT NOT NULL DEFAULT 0,
reorder_level INT NOT NULL DEFAULT 10,
max_capacity INT NOT NULL DEFAULT 100,
last_updated TIMESTAMP DEFAULT NOW(),
version BIGINT DEFAULT 0, -- Optimistic locking
PRIMARY KEY (store_id, sku_id)
);
-- Index for low-stock alerts
CREATE INDEX idx_low_stock ON store_inventory(store_id, sku_id)
WHERE available_qty <= reorder_level;
-- Reservation tracking
CREATE TABLE inventory_reservations (
reservation_id UUID PRIMARY KEY,
order_id VARCHAR(50),
store_id VARCHAR(20),
sku_id VARCHAR(50),
quantity INT NOT NULL,
status VARCHAR(20), -- SOFT, HARD, COMMITTED, RELEASED
created_at TIMESTAMP DEFAULT NOW(),
expires_at TIMESTAMP,
UNIQUE(order_id, sku_id)
);Selecting the optimal dark store is critical for meeting the 10-minute delivery promise. It's not just about proximity.
-- Find nearby dark stores within 3km radius
SELECT
ds.store_id,
ds.store_name,
ST_Distance(
ds.location::geography,
ST_MakePoint(:customer_lng, :customer_lat)::geography
) AS distance_meters,
ds.current_order_count,
ds.available_riders
FROM dark_stores ds
WHERE ST_DWithin(
ds.location::geography,
ST_MakePoint(:customer_lng, :customer_lat)::geography,
3000 -- 3km radius
)
AND ds.is_operational = true
ORDER BY distance_meters ASC
LIMIT 5;
-- Create spatial index for fast queries
CREATE INDEX idx_store_location
ON dark_stores USING GIST (location);Pro Tip: Cache the store selection result for 30 seconds. A customer browsing products doesn't need real-time store switching on every page view. Only recompute at checkout.
During flash sales (e.g., "₹1 deals at 12 PM"), traffic can spike 10-50x within seconds. Millions of users compete for limited inventory. Your system must handle:
Cache static content and product data at the edge to reduce origin load by 80%.
# Cloudflare/Akamai Edge Rules - Cache product images: 24 hours - Cache product details: 60 seconds (stale-while-revalidate) - Cache inventory count: DO NOT CACHE (always fresh) - Rate limit per IP: 100 req/min on /checkout endpoint
Instead of letting everyone hit the checkout simultaneously, implement a fair queuing system.
// Virtual Queue Implementation 1. User clicks "Add to Cart" for flash sale item 2. System checks current queue position 3. If queue_position < MAX_CONCURRENT_CHECKOUTS: - Allow immediate checkout - Reserve inventory 4. Else: - Issue queue token with estimated wait time - Show "You are #423 in queue, ~2 min wait" - Websocket updates position in real-time 5. When slot opens: - Notify user via websocket - They have 60 seconds to complete checkout - If expired, release slot to next in queue
For high-demand items, pre-generate inventory tokens before the sale starts.
-- Pre-generate tokens before flash sale
INSERT INTO inventory_tokens (token_id, sku_id, store_id, status)
SELECT
gen_random_uuid(),
'FLASH_ITEM_001',
store_id,
'AVAILABLE'
FROM dark_stores, generate_series(1, stock_count);
-- Claim token atomically (no race condition)
UPDATE inventory_tokens
SET status = 'CLAIMED',
claimed_by = :user_id,
claimed_at = NOW()
WHERE token_id = (
SELECT token_id FROM inventory_tokens
WHERE sku_id = 'FLASH_ITEM_001'
AND status = 'AVAILABLE'
LIMIT 1
FOR UPDATE SKIP LOCKED -- Key: Skip locked rows
)
RETURNING token_id;FOR UPDATE SKIP LOCKED is the secret sauce. It allows concurrent transactions to each grab a different available token without blocking each other.
Isolate failures and prevent cascade effects.
Circuit Breaker
If payment service fails 50% of requests, stop sending traffic for 30 seconds. Show "Payment temporarily unavailable" instead of timeouts.
Bulkhead Pattern
Allocate separate thread pools for critical paths. Flash sale traffic shouldn't affect regular order processing.
# Kubernetes HPA for Order Service
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: order-service-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: order-service
minReplicas: 10
maxReplicas: 100
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 60
- type: Pods
pods:
metric:
name: requests_per_second
target:
type: AverageValue
averageValue: 100
behavior:
scaleUp:
stabilizationWindowSeconds: 0 # Scale up immediately
policies:
- type: Percent
value: 100 # Double pods instantly
periodSeconds: 15
scaleDown:
stabilizationWindowSeconds: 300 # Wait 5 min before scaling downEvery second counts in quick commerce. The order-to-delivery pipeline must be optimized for speed.
PLACED (0 min)
│
▼ [Payment Confirmed]
CONFIRMED (0-1 min)
│
▼ [Assigned to Picker]
PICKING (1-3 min)
│
▼ [All items collected]
PACKED (3-5 min)
│
▼ [Rider assigned & arrived]
DISPATCHED (5-7 min)
│
▼ [Rider reaches customer]
OUT_FOR_DELIVERY (7-12 min)
│
▼ [Customer confirms receipt]
DELIVERED (10-15 min)
Parallel Track:
PLACED → CANCELLED (Customer/System cancellation)
PICKING → PARTIALLY_FULFILLED (Some items out of stock)
Items are sorted by their physical location in the store to minimize picker walking distance.
Order Items (sorted by aisle): 1. Aisle A, Shelf 2: Milk (x2) 2. Aisle A, Shelf 5: Bread 3. Aisle B, Shelf 1: Eggs 4. Aisle C, Shelf 3: Chips 5. Aisle D, Shelf 2: Coke (x3) Estimated pick time: 2 min 15 sec
Each item is scanned to confirm correct product and update inventory in real-time.
Optimization: Pre-assign delivery partner while picking is in progress. Rider should be waiting at the store by the time packing is complete. This saves 2-3 minutes.
function findOptimalRider(order, store) {
// Get available riders within 2km of store
const nearbyRiders = await getRidersNear(store.location, 2000);
// Filter by availability
const availableRiders = nearbyRiders.filter(r =>
r.status === 'IDLE' ||
(r.status === 'DELIVERING' && r.estimatedFreeIn < 3) // Free in 3 min
);
// Score each rider
const scoredRiders = availableRiders.map(rider => ({
rider,
score: calculateScore(rider, order, store)
}));
// Sort by score (higher is better)
scoredRiders.sort((a, b) => b.score - a.score);
return scoredRiders[0]?.rider;
}
function calculateScore(rider, order, store) {
let score = 0;
// Distance to store (40% weight)
const distToStore = getDistance(rider.location, store.location);
score += (1 - distToStore / 2000) * 40; // Closer = higher score
// Rider rating (20% weight)
score += (rider.rating / 5) * 20;
// Current battery/fuel (10% weight)
score += (rider.batteryPercent / 100) * 10;
// Historical performance for this route (30% weight)
const avgDeliveryTime = getHistoricalTime(rider.id, store.id, order.deliveryArea);
score += (1 - avgDeliveryTime / 15) * 30; // Faster = higher score
return score;
}During peak hours, a single rider can deliver 2-3 orders in the same direction. This improves efficiency but requires careful SLA management.
Rule: Only batch if all orders can still be delivered within 15 min SLA
Rider location updates pushed every 5 seconds via WebSocket. Customer sees live map with ETA.
Tech: Redis Pub/Sub for location broadcasts, geofencing for arrival detection
With 23,000+ reads/sec during peak, aggressive caching is essential. But inventory data must be real-time.
CDN Edge Cache (Cloudflare)
Static assets, product images, category pages
TTL: 1 hour | Hit Rate: 95%
Application Cache (Local Memory)
Hot product details, pricing, store info
TTL: 30 seconds | Hit Rate: 80%
Distributed Cache (Redis Cluster)
Inventory counts, user sessions, cart data
TTL: Real-time for inventory | Hit Rate: 99%
Database (PostgreSQL)
Source of truth for all data
Only hit on cache miss | <5% of traffic
Critical Rule: Never cache inventory availability for more than 1 second. Stale inventory data leads to overselling and customer frustration. Use Redis with write-through caching.
-- Dark Stores
CREATE TABLE dark_stores (
store_id VARCHAR(20) PRIMARY KEY,
store_name VARCHAR(100),
location GEOGRAPHY(Point, 4326),
city_id VARCHAR(10),
is_operational BOOLEAN DEFAULT true,
operating_hours JSONB,
created_at TIMESTAMP DEFAULT NOW()
);
-- Products (Master Catalog)
CREATE TABLE products (
sku_id VARCHAR(50) PRIMARY KEY,
name VARCHAR(200),
category_id VARCHAR(20),
brand VARCHAR(100),
mrp DECIMAL(10,2),
weight_grams INT,
image_urls TEXT[],
search_vector TSVECTOR, -- For full-text search
created_at TIMESTAMP DEFAULT NOW()
);
-- Orders (Sharded by city_id)
CREATE TABLE orders (
order_id UUID PRIMARY KEY,
user_id UUID,
store_id VARCHAR(20),
city_id VARCHAR(10), -- Sharding key
status VARCHAR(30),
total_amount DECIMAL(10,2),
delivery_address JSONB,
delivery_location GEOGRAPHY(Point, 4326),
placed_at TIMESTAMP DEFAULT NOW(),
delivered_at TIMESTAMP,
CONSTRAINT fk_store FOREIGN KEY (store_id) REFERENCES dark_stores(store_id)
);
-- Order Items
CREATE TABLE order_items (
order_id UUID REFERENCES orders(order_id),
sku_id VARCHAR(50),
quantity INT,
unit_price DECIMAL(10,2),
status VARCHAR(20), -- FULFILLED, SUBSTITUTED, CANCELLED
PRIMARY KEY (order_id, sku_id)
);
-- Sharding Strategy: Citus on PostgreSQL
SELECT create_distributed_table('orders', 'city_id');
SELECT create_distributed_table('order_items', 'order_id',
colocate_with => 'orders');Stock-outs are revenue killers. ML-based demand forecasting ensures each dark store has the right inventory.
Historical Sales
Last 90 days of order data per SKU per store
Seasonality
Day of week, time of day, holidays, events
External Factors
Weather, cricket matches, flash sale schedule
-- Daily job: Generate replenishment orders
WITH forecasted_demand AS (
SELECT
store_id,
sku_id,
predicted_sales_next_24h,
predicted_sales_next_72h
FROM ml.demand_forecasts
WHERE forecast_date = CURRENT_DATE
),
current_stock AS (
SELECT store_id, sku_id, available_qty
FROM store_inventory
)
SELECT
cs.store_id,
cs.sku_id,
fd.predicted_sales_next_72h - cs.available_qty AS replenishment_qty
FROM current_stock cs
JOIN forecasted_demand fd USING (store_id, sku_id)
WHERE cs.available_qty < fd.predicted_sales_next_24h * 1.5 -- Safety buffer
AND fd.predicted_sales_next_72h > cs.available_qty;Practice with an AI interviewer and get instant feedback on your system design skills.