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.
DCs within 1 hour.
Union inventory.
ACID, no oversell.
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 — 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)
Inventory lookup
Availability path end-to-end
- Client hits Availability Service with lat/lng (+ filters).
- Nearby Service returns serviceable DC IDs (start with Haversine; upgrade travel time later).
- Query Inventory (+ Items) for those DCs; aggregate by item.
- Return paginated results.
HLD — place an order
Strong consistency: check stock, create order, mark inventory — atomically. Latency can be slower than availability; correctness cannot.
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).
Putting it together — initial solution
- 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.
- 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
- 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
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.
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.
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.
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.
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.