Design Gopuff — availability, orders, and micro distribution centers

Design Gopuff

Junior-friendly Gopuff walkthrough: Item vs Inventory, nearby DCs for 1-hour delivery, availability union, ACID orders without double-booking, travel-time pruning, Redis cache + replicas for ~20k QPS, and interview bars by level.

Understanding the problem

Gopuff delivers convenience-store goods via rapid delivery and a dense network of micro-warehouses. This interview focuses on aggregating availability and placing orders without overselling — not building DoorDash routing or a full product catalog.

User location unions inventory from nearby DCs then places an order.
Nearby dark stores → union stock for your pin → order without double-booking.
01Nearby

DCs within 1 hour.

02Availability

Union inventory.

03Order

ACID, no oversell.

04Scale

Cache + replicas.

Pair with Delivery framework, Scaling reads, Contention, Caching, and Proximity search.

Functional requirements

Non-functional requirements

Core entities

Nouns first — full columns later. The critical distinction:

Item, Inventory, DistributionCenter, and Order entities.
Item = type (Cheetos) · Inventory = physical stock at a DC · Order = reserved units.
  • Item — catalog type customers care about.
  • Inventory — physical instance of an item at a DC (sum these for availability).
  • DistributionCenter — where inventory lives; filters by deliverability.
  • Order — collection of inventory units for a user (+ shipping/billing stubs).

API

Two endpoints. Pass location on both — orders must confirm nearby stock before commit. Paginate availability.

GET /availability?lat=&lng=&query=&page= -> ItemAvailability[]
// quantity = sum of inventory at DCs within 1 hour

POST /orders
{
  "lat": number,
  "lng": number,
  "items": [ { "itemId": "…", "qty": 1 } ]
}
-> Order

HLD — query availability

Two steps under 100ms end-to-end: (1) find DCs that can deliver in 1 hour, (2) sum inventory for those DCs and return the union.

Nearby DCs (primitive)

Nearby Service querying DC Table with Lat and Long.
Primitive Nearby Service — DC Table with Id, Lat, Long (crow-flies first).

Inventory lookup

Availability Service queries Item and Inventory tables by DC and gets ITEM to QTY map.
Inventory Lookup — query by [DC], return [ {{ ITEM: QTY }} ].

Availability path end-to-end

User through API Gateway to Availability Service, Nearby Service and DC Table, Item and Inventory tables.
All Together — Gateway → Availability → Nearby DCs → Item/Inventory union.
  1. Client hits Availability Service with lat/lng (+ filters).
  2. Nearby Service returns serviceable DC IDs (start with Haversine; upgrade travel time later).
  3. Query Inventory (+ Items) for those DCs; aggregate by item.
  4. Return paginated results.

HLD — place an order

Strong consistency: check stock, create order, mark inventory — atomically. Latency can be slower than availability; correctness cannot.

Orders Service runs a single Postgres transaction to check stock and commit order.
Orders Service → single Postgres transaction on the leader.

Distributed lock across two stores (good, messy)

Separate inventory DB + orders DB with locks. Failure modes: crash after order insert before decrement; deadlocks on overlapping carts. Fixable, but interview time sinks.

Single Postgres transaction (great)

Colocate inventory + orders. One transaction (e.g. SERIALIZABLE / row locks): check qty → mark ordered → insert Order/OrderItems → commit. Second concurrent buyer fails cleanly. Prefer failing the whole multi-item order over partial success (device without battery).

Timeline comparing a distributed lock failure mode against a single Postgres transaction rollback.
Why the transaction wins — a crash mid-lock leaves a half-applied state; a crash mid-transaction rolls back cleanly.

Putting it together — initial solution

Initial Solution: User, API Gateway, Availability, Nearby, Orders services with DC Table and Item Inventory Orders schemas.
Initial Solution — Availability reads · Orders write transaction · shared Nearby + DC Table.
  • Availability Service — read path, uses Nearby + inventory replicas.
  • Orders Service — write path, ACID on Postgres leader.
  • Nearby Service — shared DC lookup for both.
  • Partition inventory by region when you scale — foundation first.

Deep dive — traffic and drive time

Crow-flies distance lies: a DC across a river can be close in miles and useless in drive time. FR says 1 hour drive.

Three approaches: Haversine only, travel API for all DCs, prune then travel API.
Prune candidates (~60 mi) then call Travel Time Service — not all 10k DCs.
  • Bad: SQL Haversine only — ignores roads/traffic.
  • Bad: Travel-time API against every DC — too many calls (DCs are buildings; sync to memory ~5 min, but still don't query all).
  • Great: Keep DCs in Nearby Service memory → prune to optimistic radius (e.g. 60 miles) → travel-time API only for candidates → keep those ≤ 1 hour.

Deep dive — fast, scalable availability

Napkin from 10M orders/day: ~10 pages viewed per purchase, ~5% convert → ~20k availability QPS. DB alone won't love that.

10M orders/day / 100k sec/day * 10 pages / 0.05 buy-rate ≈ 20k QPS
Capacity sketch: napkin QPS math, nearby DC pruning, and caching the survivors.
The whiteboard version of the math above — say it before you draw a single box.
Availability Service through Redis to Inventory replicas sharded by REGION_ID.
Query Inventory Through Cache — Redis TTL ~1m · replicas · shard by REGION_ID · invalidate on order.
  • Redis cache-aside — key by location/DCs (+ filters); miss → DB → set ~1 min TTL.
  • Invalidate on order — Orders Service expires keys for affected DCs/items on commit.
  • Read replicas — availability can be slightly stale; orders always hit the leader.
  • Partition by region — e.g. zip prefix / region ID so each query hits 1–2 partitions.

Final design

Final Nearby Service: Gateway, Availability, Nearby, Travel Time Service, Inventory with read replicas and REGION_ID sharding.
Final Nearby Service — Travel Time Service + Inventory read replicas sharded by REGION_ID.

Cost and performance levers

The line item people forget: the Travel Time API is a paid third-party call, and pruning to ~60 miles before calling it isn't just a latency trick — it's the difference between a few dozen billed lookups per availability query and thousands. Same logic on the cache: a short TTL on 100k items across 10k DCs is a modest Redis footprint, but a naive "cache everything forever" policy grows unbounded as promos rotate stock, so TTL is a cost lever as much as a freshness one.

What interviewers expect by level

Architecture by level

Three progressive sketches — beginner spine, mid optimizations, pro production depth. Use the matching diagram in the interview; say the tradeoff out loud.

Beginner Gopuff: Availability and Orders on Postgres.
Beginner architecture

Pros: Item vs Inventory clarity + one ACID order transaction prevents double-booking without distributed locks. Cons: Haversine-only nearby ignores roads; availability QPS will melt a single Postgres under promo traffic.

Mid Gopuff: Nearby service and Redis cache.
Mid architecture

Pros: Short-TTL Redis + invalidate-on-order matches ~20k availability QPS; Nearby service keeps the read path honest. Cons: Cache can show stock that's already sold — fine for browse, fatal if orders reuse the cache; crow-flies still lie about drive time.

Pro Gopuff: travel-time prune and region-sharded replicas.
Pro architecture

Pros: Prune then travel-time API is correct and cheaper than calling every DC; region shards keep queries local. Cons: Third-party travel APIs cost money and fail; reassigning DCs across partition boundaries needs a migration story.

Side-by-side sketch of the availability read path versus the ACID order write path for interview Q&A.
The split every level below is graded on — AP browse on the left, ACID checkout on the right.
Interview takeaway

Mid: working spine. Senior: optimize both paths. Staff: insight from load math and failure modes.

Wrapping up

Gopuff is a read/write split story: soft real-time union of nearby inventory for browse, hard ACID reservation for checkout — with travel time and short-TTL caches making the read path honest and cheap.

Related: Delivery framework · Scaling reads · Caching · Contention · Proximity search · PostgreSQL · Design Yelp · Design Ticketmaster.

← Lattice