Skip to content
System design course
Ch.4 · Designing real systems·how to build it ·7 min read

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.