Building DoorDash (food delivery)
Implement the order saga with compensations, prep-time-aware courier dispatch, and delivery batching.
The order saga
The order is a durable workflow of idempotent steps; any failure triggers compensations:
def place_order(customer, restaurant_id, items):
order = orders.create(state="created", customer=customer, items=items)
saga = Saga(order.id, steps=[
Step("authorize_payment", do=auth_payment, undo=void_auth),
Step("notify_restaurant", do=send_to_restaurant, undo=cancel_restaurant),
Step("await_accept", do=wait_for_accept, undo=refund),
Step("dispatch_courier", do=dispatch_timed, undo=release_courier),
])
saga.run() # each step idempotent; persisted
return order.id
class Saga:
def run(self):
for i, step in enumerate(self.steps):
try:
step.do(self.ctx)
self.persist(completed=i) # durable progress (resume on crash)
except StepFailed:
for done in reversed(self.steps[:i]): # compensate completed steps
done.undo(self.ctx)
self.mark_failed(); return
Persisting progress makes the saga resumable after a crash; compensations (void auth, refund, release courier) keep the three parties consistent without a global transaction.
Prep-time-aware courier dispatch
Dispatch so the courier arrives ~when the food is ready — reuse Uber’s geo index + lease, but schedule by estimated readiness:
def dispatch_timed(ctx):
ready_at = now() + estimate_prep_time(ctx.restaurant, ctx.items)
travel = estimate_travel(courier_pool_near(ctx.restaurant), ctx.restaurant)
dispatch_at = ready_at - travel # aim arrival ≈ ready time
schedule(dispatch_at, lambda: lease_and_offer(ctx)) # job scheduler
Too early → courier waits; too late → cold food. The estimate is refined as the restaurant updates prep status.
Courier matching (reused)
def lease_and_offer(ctx):
for courier in rank_by_eta(find_available_couriers_near(ctx.restaurant)):
if lease.acquire(f"courier:{courier}", ttl="20s") and offer(courier, ctx):
assign(ctx.order, courier); return
retry_with_wider_radius(ctx)
Same in-memory geo index + lease dispatch as Uber — no double-assignment.
Delivery batching
Group orders that can share a route within time windows to boost courier efficiency:
def try_batch(new_order, courier):
current = courier.active_orders
if (len(current) < MAX_BATCH
and same_direction(current, new_order)
and within_time_windows(current + [new_order])):
courier.add(new_order) # piggyback on the existing route
return True
return False
Batching is a constrained optimization (route + freshness windows); even a greedy “compatible nearby order” heuristic captures much of the gain.
Live tracking and updates
Customer and restaurant subscribe (pub/sub) to order state and the courier’s location stream — reuse the real-time delivery channel keyed by order id.
Scale and failure handling
- Restaurant rejects / unavailable → saga compensates (refund), suggests alternatives.
- Courier drops / no-show → lease expires → re-dispatch (saga step retried).
- Payment failure → saga aborts before sending to the restaurant.
- Crash mid-order → saga resumes from persisted progress (idempotent steps).
- Location firehose → in-memory geo index (Uber pattern); history streamed off.
- Peak (lunch/dinner) → autoscale matching + saga workers; surge couriers.
The takeaway
Concrete signals: a resumable saga with idempotent steps + compensations for the three-party order, prep-time-aware courier dispatch reusing geo index + lease matching, and delivery batching for efficiency. The saga (workflow over multiple services without a global transaction) is the reusable orchestration pattern for any multi-party transaction.