Zepto API architecture: every call behind one order Every call behind a quick-commerce order: search, reserve, charge, commit, order and outbox, then the store and a rider, plus retry and refund.
Successful order Here’s one Zepto order, call by call. Your app searches first: GET /search for milk, with your location. The gateway asks Search, which finds the dark store that serves your address, and answers: three items, with their live price and stock. The results come back to your phone: your basket, 184 rupees. Then you place the order: POST /orders, carrying an idempotency key, so a double tap can never create two orders. The gateway passes it to Checkout, which saves the draft once under that key, in DynamoDB. Checkout asks Inventory to hold your three items for ten minutes, and Inventory answers with hold 77: the stock and the hold changed in one transaction. Now Checkout asks Payment to charge 184 rupees, as a new payment attempt over UPI. Payment sends a UPI collect request to the provider. The answer, captured, comes as a signed webhook, verified, and deduplicated by its event ID. Payment tells Checkout the money is in: paid. Only then does Checkout make the hold final, and Inventory confirms. Checkout asks the Order service for exactly one order per checkout, and Order answers: order 42, saved with an outbox row in the same transaction. Checkout tells the gateway: confirmed. And your phone gets a two oh one: order placed, arriving in ten minutes. Meanwhile, the outbox publishes Order Confirmed through Kafka to Fulfilment. Fulfilment gives one picker the task: three items scanned, and the bag sealed. Only then does it look for a rider: Dispatch takes fresh rider locations from Redis GEO, and ranks them by pickup time. The best rider gets the offer and accepts, and Dispatch saves one durable assignment. At the store, the rider scans the bag, and Delivery verifies the pickup. And your phone hears it over a live stream: Rohit has picked it up, nine minutes away. Twenty-four calls, and your phone made just two of them: the search, and the order. Every call, in order You → API Gateway: GET /search?q=milk — lat 12.97, lng 77.64 (REST · HTTPS) API Gateway → Search: search(milk) — serviceable store: BLR-07 (Internal RPC) Search → API Gateway: 3 items — price + live stock at BLR-07 (reply, Internal RPC) API Gateway → You: 200 · results — [{ sku, name, price, in_stock }] (reply, REST · HTTPS) You → API Gateway: POST /orders — Idempotency-Key: ck_042 · { cart: 3 items, address } (REST · HTTPS) API Gateway → Checkout: place order — draft saved once under ck_042 · DynamoDB (Internal RPC) Checkout → Inventory: reserve — BLR-07 · 3 SKUs · hold for 10 min (Internal RPC) Inventory → Checkout: hold_77 — stock and hold change in one transaction (reply, Internal RPC) Checkout → Payment: charge ₹184 — attempt pay_091 · UPI (Internal RPC) Payment → Payment provider: UPI collect — ₹184 · dk@okaxis (REST · HTTPS) Payment provider → Payment: captured — signed webhook · verified · event_id deduped (reply, REST · HTTPS) Payment → Checkout: paid (reply, Internal RPC) Checkout → Inventory: commit hold_77 (Internal RPC) Inventory → Checkout: committed (reply, Internal RPC) Checkout → Order: create order — one order per checkout_id (Internal RPC) Order → Checkout: order_042 — order + outbox row · one transaction (reply, Internal RPC) Checkout → API Gateway: confirmed (reply, Internal RPC) API Gateway → You: 201 · order_042 — { status: confirmed, eta: 10 min } (reply, REST · HTTPS) Order → Fulfilment: OrderConfirmed — outbox → Kafka → Fulfilment (Internal RPC) Fulfilment → Fulfilment: pick + pack — one leased picking task · 3 items scanned · bag sealed (Internal RPC) Fulfilment → Fulfilment: find a rider — bag sealed → Delivery asks Dispatch · Redis GEO → fresh riders → rank by pickup ETA (Internal RPC) Fulfilment → Fulfilment: offer → accept → persist claim — rider acceptance returns to Dispatch · one durable assignment (Internal RPC) Fulfilment → Fulfilment: verify pickup — Delivery holds OrderPacked + RiderAssigned · the rider scans the bag (Internal RPC) Fulfilment → You: event: picked_up — data: { rider: "Rohit", eta: "9 min" } (reply, SSE stream) Payment fails · retry Here’s a Zepto order whose first payment fails. Keep an eye on the retry. Your app searches for milk first. The basket comes back: three items, 184 rupees. You place the order: POST /orders, with an idempotency key. Checkout saves the draft once under that key, in DynamoDB. It asks Inventory to hold your three items for ten minutes, and Inventory answers with hold 77. Checkout asks Payment to charge 184 rupees, and Payment sends a UPI collect request to the provider. This time, the bank declines: insufficient funds, in a signed webhook. Payment records the failed attempt in its ledger, and tells Checkout. Checkout stops at payment failed, but keeps the hold. Your phone gets a four oh two: retry, with the hold expiring in nine minutes. You tap retry: POST /payments, for the same checkout, attempt two, with its own idempotency key. Checkout finds its saga where it stopped. The hold is still valid: no new draft, and no new hold. A new attempt goes to Payment, and on to the provider as another UPI request. This time it’s captured and verified, and Checkout hears: paid, one capture on this checkout. Only then is the hold made final, and Inventory confirms. The Order service creates exactly one order, saved with its outbox row in one transaction. Checkout confirms, and your phone gets a two oh one: order placed. The outbox sends Order Confirmed to Fulfilment, and the order is packed and handed to a rider. Your phone hears it on its live stream: Rohit is on the way. One declined attempt, one retry, and one charge: the hold kept your eggs, and the key kept you from paying twice. Every call, in order You → API Gateway: GET /search?q=milk — lat 12.97, lng 77.64 · store BLR-07 (REST · HTTPS) API Gateway → You: 200 · 3 items — price + live stock (reply, REST · HTTPS) You → API Gateway: POST /orders — Idempotency-Key: ck_042 · { cart: 3 items, address } (REST · HTTPS) API Gateway → Checkout: place order — draft saved once under ck_042 · DynamoDB (Internal RPC) Checkout → Inventory: reserve — BLR-07 · 3 SKUs · hold for 10 min (Internal RPC) Inventory → Checkout: hold_77 — stock and hold change in one transaction (reply, Internal RPC) Checkout → Payment: charge ₹184 — attempt pay_091 · UPI (Internal RPC) Payment → Payment provider: UPI collect — ₹184 · dk@okaxis (REST · HTTPS) Payment provider → Payment: declined — signed webhook · reason: insufficient_funds · event_id deduped (reply, REST · HTTPS) Payment → Checkout: failed · pay_091 — attempt recorded in the payment ledger (reply, Internal RPC) Checkout → API Gateway: 402 · payment_failed — saga at PAYMENT_FAILED · hold_77 kept until it expires (reply, Internal RPC) API Gateway → You: 402 · payment failed — { retryable: true, hold_expires_in: 540 } (reply, REST · HTTPS) You → API Gateway: POST /payments — { checkout: ck_042, attempt: 2 } · Idempotency-Key: ck_042/2 (REST · HTTPS) API Gateway → Checkout: resume ck_042 — saga found at PAYMENT_FAILED · hold_77 still valid · no new draft, no new hold (Internal RPC) Checkout → Payment: charge ₹184 — attempt pay_092 · UPI (Internal RPC) Payment → Payment provider: UPI collect — ₹184 · dk@okaxis (REST · HTTPS) Payment provider → Payment: captured — signed webhook · verified · event_id deduped (reply, REST · HTTPS) Payment → Checkout: paid · pay_092 — one capture on ck_042 (reply, Internal RPC) Checkout → Inventory: commit hold_77 (Internal RPC) Inventory → Checkout: committed (reply, Internal RPC) Checkout → Order: create order — one order per checkout_id (Internal RPC) Order → Checkout: order_042 — order + outbox row · one transaction (reply, Internal RPC) Checkout → API Gateway: confirmed (reply, Internal RPC) API Gateway → You: 201 · order_042 — { status: confirmed, eta: 10 min } (reply, REST · HTTPS) Order → Fulfilment: OrderConfirmed — outbox → Kafka → pack · then find a rider · pickup (Internal RPC) Fulfilment → You: event: picked_up — data: { rider: "Rohit", eta: "9 min" } (reply, SSE stream) Items missing at pickup Here’s a Zepto order where something is missing in the store. Keep an eye on the refund. You place the order: POST /orders. It goes through as usual: held, charged 184 rupees, and confirmed. Your phone gets a two oh one: order placed. The outbox sends Order Confirmed through Kafka to Fulfilment. A picker scans the three items. Milk, bread, and then the eggs: the shelf is empty. Two of three found. Fulfilment records the shortage on the picking task, and the bag will be sealed with two items. It tells the Order service through Kafka: Item Unavailable, eggs, one. The Order service amends the order to version two, 84 rupees, with the change in its outbox, in one transaction. Then the saga compensates. Inventory voids the eggs line, so its ledger matches the shelf. Payment is asked to refund exactly 100 rupees, against the original payment, with its own idempotency key. Payment sends a partial refund over UPI, and the provider accepts it, in a signed webhook. Payment’s ledger now reads: 184 captured, 100 refunded. Only now does your phone hear: two items, 84 rupees, 100 refunded. Then, and only then, the bag is sealed with two items, a rider is claimed, and the pickup is verified. And your phone hears it on its live stream: two items, on the way, nine minutes. One empty shelf, one amended order, one exact refund: you pay only for what arrives. Every call, in order You → API Gateway: POST /orders — Idempotency-Key: ck_042 · { cart: 3 items } (REST · HTTPS) API Gateway → You: 201 · order_042 — hold → charge ₹184 → commit → order + outbox (reply, REST · HTTPS) Order → Fulfilment: OrderConfirmed — outbox → Kafka → Fulfilment (Internal RPC) Fulfilment → Fulfilment: scan 3 SKUs — picker at BLR-07 · eggs: shelf empty · 2 of 3 found (Internal RPC) Fulfilment → Fulfilment: record shortage — task_311 · eggs ×1 · seal the bag with 2 items (Internal RPC) Fulfilment → Order: ItemUnavailable — via Kafka · { order_042, sku: eggs, qty: 1, event_id } (Internal RPC) Order → Order: amend → v2 — ₹184 → ₹84 · order v2 + OrderAmended in the outbox · one transaction (Internal RPC) Order → Inventory: void eggs line — hold_77's committed line → voided · eggs out of stock at BLR-07 (Internal RPC) Inventory → Order: voided — the ledger now matches the shelf (reply, Internal RPC) Order → Payment: refund ₹100 — Idempotency-Key: refund/order_042/eggs · against pay_091 (Internal RPC) Payment → Payment provider: refund ₹100 — partial refund · UPI (REST · HTTPS) Payment provider → Payment: refund accepted — signed webhook · verified · event_id deduped (reply, REST · HTTPS) Payment → Order: refunded ₹100 — ledger: captured ₹184 · refunded ₹100 (reply, Internal RPC) Order → You: event: order_updated — data: { items: 2, total: "₹84", refunded: "₹100" } (reply, SSE stream) Fulfilment → Fulfilment: seal bag · rider claimed · pickup verified — then, and only then: 2 items packed, one rider, pickup (Internal RPC) Fulfilment → You: event: picked_up — data: { items: 2, eta: "9 min" } (reply, SSE stream)