Learning pathsA
Question Breakdowns

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

Contract / pseudocode
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

Contract / pseudocode
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

Three architecture decisions for Local Delivery Service, including the pressure each introduces.
Scroll to inspect the diagram, or open it at full size.

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

Connected responsibilities for Local Delivery Service. Trace the authoritative and derived paths separately.
Scroll to inspect the diagram, or open it at full size.

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

Local Delivery Service: baseline request paths.
Scroll to inspect the diagram, or open it at full size.
  1. Buyer → order API → order and outbox.
  2. Workflow → inventory reservation → payment authorization.
  3. Dispatcher → courier offer → conditional assignment.

Evolve the design under load

Local Delivery Service: additional scaling and recovery paths.
Scroll to inspect the diagram, or open it at full size.
  1. Location stream → geospatial candidate index.
  2. City queues → bounded dispatch workers.
  3. 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

MechanismWhat it decidesFailure to handle
Spatial lookupWho might be nearbyStale and boundary candidates
Assignment ownerWho owns the order/courierConcurrent acceptances
Trip event streamWhat happened and whenDisconnect and replay
Support repairHow exceptional cases closeUnlogged 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.

sql
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.

Checkout saga without a network call inside SQL. Customer to Order/stock authority: Submit stable request and unexpired quote; Order/stock authority to Order/stock authority: Reserve every SKU; persist payment intent/outbox; Order/stock authority to Payment provider: Authorize using original operation key; Payment provider to Order/stock authority: Timeout means unknown; query same operation; Order/stock authority to Dispatcher: Dispatch only after confirmed payment and valid stock; Dispatcher to Order/stock authority: Conditional courier and order assignment; Order/stock authority to Customer: Return tracked order state and version
Scroll to inspect the diagram, or open it at full size.

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.

Order saga transitions and compensation. Stock held / Payment success before deadline / Paid; emit dispatch task; Payment unknown / Late provider success after stock expired / Refund pending; do not dispatch; Dispatching / Offer accept with current order/courier version / Assigned; reject competing acceptance; Assigned / Courier disconnect / Track uncertainty; reassign only after old ownership revoked; Delivered / Duplicate completion / Return original completion; no second settlement
Scroll to inspect the diagram, or open it at full size.

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

Provider idempotency contract example.

PostgreSQL transaction isolation and concurrent updates.

Your study notes