Zepto system design: one grocery order, from basket to rider
Follow one grocery basket through search, an inventory hold, UPI payment, an order saga with an outbox, packing and rider dispatch.
Successful order
- You tap add to cart, and the clock starts: ten minutes, door to door.
- A load balancer picks a healthy server, and the API gateway checks your login and rate limit.
- First: can anyone reach you in ten minutes? Serviceability finds the dark store that covers your address.
- Search looks only in that store’s own index, and Enrichment adds the live price and stock.
- Your basket comes back: three items, 184 rupees, all in stock.
- Placing the order starts a checkout saga: a chain of saved steps, so a failure resumes instead of starting over.
- Its draft is saved to DynamoDB once, so a double tap never makes two checkouts.
- Inventory holds your items for ten minutes, in one atomic step, so nobody grabs the last pack of eggs while you pay.
- Payment asks your UPI app for 184 rupees, through the payment provider.
- The money is captured, but Payment trusts only the provider’s signed callback, never the phone.
- Only then is the hold made final, and the Order service creates exactly one order.
- The order and an outbox row commit in one transaction, so the event that starts packing is never lost.
- The reply goes back the way it came: order placed.
- Meanwhile, the outbox publishes an Order Confirmed event to Kafka.
- Fulfilment creates a picking task. A picker scans all three items and seals the bag.
- With the bag sealed, Delivery asks for a rider.
- Dispatch asks Redis which riders are near the store right now, and ranks them by pickup time.
- The quickest one gets the offer, and accepts.
- Dispatch saves one assignment, so two riders can never claim the same bag.
- Delivery now has a sealed bag and a rider, and verifies the pickup.
- Your phone is told: on the way, nine minutes. Eighteen systems and two people, for one basket.
Payment fails · retry
- Same basket, same ten-minute promise. But this time, watch the payment.
- A load balancer picks a healthy server, and the API gateway checks your login and rate limit.
- First: can anyone reach you in ten minutes? Serviceability finds the dark store that covers your address.
- Search looks only in that store’s own index, and Enrichment adds the live price and stock.
- Your basket comes back: three items, 184 rupees, all in stock.
- Placing the order starts a checkout saga: a chain of saved steps, so a failure resumes instead of starting over.
- Its draft is saved to DynamoDB once, so a double tap never makes two checkouts.
- Inventory holds your items for ten minutes, in one atomic step, so nobody grabs the last pack of eggs while you pay.
- Payment asks your bank for 184 rupees, and the bank says no: insufficient funds.
- Payment records the failed attempt, but keeps the hold, so nobody else can take your eggs.
- Your phone shows the decline. Nothing was charged, and your items are still held.
- You tap retry, and the same checkout resumes against the same hold. Nothing starts over.
- It’s a new attempt with its own ID, and only one can be live at a time, so you’re never charged twice. This one is captured.
- Payment checks the signed callback, the hold is made final, and exactly one order is created.
- The order and its outbox row are saved together, and your phone shows: order placed.
- Meanwhile, the outbox publishes an Order Confirmed event to Kafka.
- Fulfilment creates a picking task. A picker scans all three items and seals the bag.
- With the bag sealed, Delivery asks for a rider.
- Dispatch asks Redis which riders are near the store right now, and ranks them by pickup time.
- The quickest one gets the offer, and accepts.
- Dispatch saves one assignment, so two riders can never claim the same bag.
- Pickup is verified, and your phone is told: on the way. One declined payment, one retry, and you paid exactly once.
Items missing at pickup
- Same basket, same ten-minute promise. This time, watch what happens in the store.
- A load balancer picks a healthy server, and the API gateway checks your login and rate limit.
- First: can anyone reach you in ten minutes? Serviceability finds the dark store that covers your address.
- Search looks only in that store’s own index, and Enrichment adds the live price and stock.
- Your basket comes back: three items, 184 rupees, all in stock.
- Placing the order starts a checkout saga: a chain of saved steps, so a failure resumes instead of starting over.
- Its draft is saved to DynamoDB once, so a double tap never makes two checkouts.
- Inventory holds your items for ten minutes, in one atomic step, so nobody grabs the last pack of eggs while you pay.
- Payment asks your UPI app for 184 rupees, through the payment provider.
- The money is captured, but Payment trusts only the provider’s signed callback, never the phone.
- Only then is the hold made final, and the Order service creates exactly one order.
- The order and an outbox row commit in one transaction, so the event that starts packing is never lost.
- The reply goes back the way it came: order placed.
- Order Confirmed reaches Kafka, and a picker starts scanning. Milk, bread, and then the eggs: the shelf is empty.
- The picker reports it. Fulfilment records the shortage and publishes an Item Unavailable event to Kafka.
- The Order service amends your order: version two, 84 rupees, saved with a new outbox row.
- Now the saga compensates, walking backwards: Inventory voids the eggs line, and Payment asks the provider to refund exactly 100 rupees.
- The refund is recorded, and only then is your phone told: 100 rupees refunded.
- Meanwhile, the picker sealed the two items that exist, and Delivery asks for a rider.
- Dispatch asks Redis which riders are near the store right now, and ranks them by pickup time.
- The quickest one gets the offer, and accepts.
- Dispatch saves one assignment, so two riders can never claim the same bag.
- Pickup is verified, and your phone is told: on the way, two items. You pay only for what arrives.