Local Delivery Service
Design a service that matches customers placing local orders with couriers, tracks pickup and delivery, and communicates changes.
Interview scope and guarantees
Browse nearby stores, place an order, reserve available items, assign a courier, and track progress. Begin with one city and one store per order. Inventory and payment need durable outcomes; location freshness can be approximate. A confirmed order must have one owning fulfillment workflow.
Capacity worksheet
Assume 50,000 active couriers reporting every 5 seconds: 10,000 location writes/s. At 200 bytes/update that is 2 MB/s before overhead. Retaining every update for a day produces 172.8 GB; a current-location projection needs far less. Assume 100 orders/s peak and keep order transactions separate from location ingestion.
Concrete API contract
GET /stores?lat=...&lon=...&radius=...
POST /orders {storeId,items,address,paymentToken} -> 202 {orderId}
POST /couriers/{id}/location {sequence,lat,lon,time}
POST /offers/{id}/accept {version}Data model and access paths
orders(order_id PK, user_id, store_id, state, version)
order_items(order_id, sku, quantity, quoted_price)
assignments(order_id UNIQUE, courier_id, lease_until, generation)
courier_location(courier_id PK, sequence, position, updated_at)
inventory(store_id,sku,available,version); PK(store_id,sku)
stock_holds(order_id,sku,qty,state,expires_at); PK(order_id,sku)
quotes(id PK,store_id,address_version,total_minor,expires_at)
payment_attempts(order_id,attempt_key,state,provider_ref); UNIQUE(order_id,attempt_key)
courier_assignments(courier_id UNIQUE,order_id UNIQUE,offer_id,version)Evolve a solution and explain each change
Figure — Three architecture decisions for Local Delivery Service, including the pressure each introduces.
Step 1: Accept an order
Persist the basket and address with a quoted service area and price. An accepted order does not reserve scarce inventory or a courier.
Step 2: Reserve explicitly
Use conditional stock reservations and a durable order state machine. A timeout can leave reserved stock without a known payment outcome.
Step 3: Coordinate fulfillment
Assign one courier through conditional ownership; reconcile payment and reservation deadlines. Dispatch projections and location updates can be stale.
Responsibility overview
Figure — Connected responsibilities for Local Delivery Service. Trace the authoritative and derived paths separately.
Worked end-to-end scenario
A customer buys the final two cartons at store S. The order service records a pending order, then atomically reserves two units while stock permits it. Payment authorization succeeds but its response is lost. The customer retry returns the same order rather than reserving again. A reconciler queries the payment attempt using its stable identity. Only an authorized order enters dispatch. Two dispatchers propose different couriers: one conditional assignment succeeds; the other reloads the order. If no courier is available before the reservation deadline, transition to a stated cancellation/refund policy instead of leaving inventory locked indefinitely.
Why these access paths matter
Read catalog by store and item; mutate available stock with an atomic quantity check, not a cached number. Index open dispatch jobs by service zone and state. Store order lines and reservation IDs so cancellation releases only the quantity held by this order. Separate approximate availability for browsing from the authoritative checkout decision.
Build the baseline first
- Buyer → order API → order and outbox.
- Workflow → inventory reservation → payment authorization.
- Dispatcher → courier offer → conditional assignment.
Evolve the design under load
- Location stream → geospatial candidate index.
- City queues → bounded dispatch workers.
- Order events → tracking projection → client updates.
Defend the hardest decision
Dispatch first selects nearby eligible couriers, then ranks a bounded candidate set by estimated travel time and workload. A location index suggests candidates; it does not grant assignment ownership. An offer acceptance must atomically check that the order is unassigned and the offer generation is current. To prevent a courier taking incompatible deliveries, validate capacity in the same authoritative assignment boundary or reserve capacity before confirming.
Failure and recovery analysis
The store rejects one item after payment authorization. The workflow must choose substitution, partial fulfillment, or cancellation; a database rollback cannot undo a provider authorization. Persist compensation intent and retry it. If a courier disconnects, mark location stale and contact/reassign according to policy; never dispatch a second courier solely because one heartbeat was missed.
Security and privacy boundary
Expose precise addresses only to authorized participants during fulfillment. Encrypt retained location history and apply a retention limit.
Interview follow-ups with reasoning
Question: What if two couriers accept?
Show answer and explanation
Answer: The conditional assignment permits only one current owner.
Question: What if location arrives out of order?
Show answer and explanation
Answer: Keep the highest accepted sequence and reject implausibly old data.
Question: How would multi-store orders change the design?
Show answer and explanation
Answer: Split fulfillment legs while retaining an order-level outcome and compensation policy.
Operate and verify the design
Order-to-assignment time, stale-driver fraction, compensation age and per-city queue delay.
Lose a courier acceptance response and verify that only one assignment remains authoritative.
A second scenario to test transfer
Two orders select courier C from the same location snapshot. Both offers are displayed, but C accepts the first under the declared policy. The owner atomically assigns C to one order; the competing acceptance fails and dispatch chooses another candidate. The second order can still display an outdated nearby courier briefly without violating assignment correctness.
Couriers C and D both appear close to an order. Two dispatchers offer C two different orders. Define the winning transition and the losing response.
Show answer and explanation
Answer: Use a versioned, conditional active-assignment transition at the courier/assignment authority. One acceptance commits; the other returns conflict and selects a new candidate. Presence or a nearby result cannot grant ownership. Persist the offer and trip event so retries return the same decision.
Compare alternatives
| Mechanism | What it decides | Failure to handle |
|---|---|---|
| Spatial lookup | Who might be nearby | Stale and boundary candidates |
| Assignment owner | Who owns the order/courier | Concurrent acceptances |
| Trip event stream | What happened and when | Disconnect and replay |
| Support repair | How exceptional cases close | Unlogged manual state changes |
A design-changing exercise
Two orders each observe five units and request four. Can both be accepted?
Show answer and explanation
Answer: Not if inventory is promised strictly. The reservation must conditionally decrement the authoritative quantity in a transaction; an earlier availability read is advisory.
Design workshop: one order, three independent authorities
Core requirements are basket checkout, stock reservation, payment, courier assignment and delivery progress. Route optimization across many stops is an extension. Assume a two-second checkout admission target, five-second location freshness and no oversold stock; discovery can lag, but reservation and assignment decisions cannot use a stale cache as authority.
Add inventory(store_id,sku,available,version), stock_holds(order_id,sku,qty,state,expires_at), quotes(id,store_id,address_version,total_minor,expires_at), payment_attempts(order_id,attempt_key,state,provider_ref) and courier_assignments(courier_id UNIQUE,order_id UNIQUE,offer_id,version). Address records are owner-scoped; quote stores an immutable address and price snapshot. A changed basket or expired quote requires a new quote, not silent price mutation.
BEGIN;
-- Deduplicate customer/request and verify quote before this step.
UPDATE inventory SET available = available - :qty,
version = version + 1
WHERE store_id = :store AND sku = :sku AND available >= :qty;
-- Require one affected row for every SKU; otherwise roll back.
INSERT INTO stock_holds VALUES (:order,:sku,:qty,'held',:deadline);
INSERT INTO order_events VALUES (:event,:order,'stock_reserved');
INSERT INTO outbox VALUES (:event,:order,'authorize_payment');
COMMIT;Lock multiple SKU rows in a stable SKU order and handle deadlock retries. The order authority stores saga state; it does not hold the stock transaction open while calling payment. A provider timeout moves to payment_unknown and a reconciler queries the original key. Confirmed payment permits dispatch. Stock expiry before a late success triggers compensation, not delivery of an unavailable basket.
Figure — Checkout saga without a network call inside SQL.
Select couriers by nearby eligible cells, filter capacity and freshness, then score estimated road ETA, current workload and assignment fairness. Straight-line distance is a cheap candidate filter, not an ETA. Expand the radius if no suitable courier accepts. For the baseline, one database transaction locks the order and courier rows and changes both from unassigned/available to assigned. This makes the two-resource invariant explicit. Split authorities need a reservation coordinator; this baseline does not pretend two independent cache writes are atomic.
Figure — Order saga transitions and compensation.
Offer acceptance returns 409 with the current order status when ownership changed. GET /orders/id returns state, version, courier freshness and a cursor for durable progress events. Manual repair uses a privileged command with actor/reason/version, not direct unlogged row edits. For 10,000 active couriers reporting every five seconds, ingest 2,000 location updates/s; keep this path separate from checkout's transactional stock load.
Exercise: Courier C is offered orders O1 and O2 and accepts both after a disconnect. Which commits?
Show answer and explanation
Answer: The first transaction claiming C and its order wins. The second reads C already assigned and returns conflict. A timeout is resolved by querying the same offer ID. Reassignment increments the ownership version so delayed acceptance cannot reclaim an order.
Technical references
Couriers C and D both appear close to an order. Two dispatchers offer C two different orders. Define the winning transition and the losing response.
Your design draft
Clarify assumptions, explain your approach, and test the difficult cases. Save your draft, then compare it with the study notes.
Self-review checklist
Self-guided practice. Automated AI feedback and code execution are not connected.