Skip to content

Design a food delivery system (Swiggy, Zomato, DoorDash)

TL;DR

  • You build the three-sided flow: a customer orders, a restaurant accepts and cooks, a courier is offered the job and delivers it.
  • Three decisions carry the interview: an order transition table as the single gate on state, a courier offer that is a time-boxed lease (so nobody is double-assigned and a silent courier does not stall the order), and claim-then-act-then-revert across two services so the cancel-versus-accept race has exactly one safe outcome.
  • Patterns that earn their place: State (the table), Strategy (courier ranking, coupons), Event Bus plus Observer (notifications), Facade (FoodDeliveryService), Factory (payment gateways), Null Object (no coupon).

Problem statement

"Design a food delivery platform. Customers browse nearby restaurants, build a cart and place an order. The restaurant accepts or rejects it and cooks. A delivery partner is offered the job, accepts, collects the food and delivers it. You take payment, apply coupons, notify all three parties and collect ratings. Focus on the order lifecycle, how a partner gets assigned, and what happens when two things happen at once — two dispatchers picking the same courier, or a customer cancelling while a courier is accepting."

Requirements

Functional

  • Browse open restaurants near a location, with menus and per-item availability.
  • A cart holds items from one restaurant; checkout snapshots prices into the order.
  • Coupons: flat off above a minimum spend, capped percentage off, free delivery.
  • Payment is authorized at checkout and captured on delivery; a rejection or cancellation voids the authorization.
  • The restaurant accepts or rejects, prepares, and marks the food ready.
  • A delivery partner is offered the order by a pluggable strategy, and may accept, decline or time out.
  • The order walks a state machine from placed to delivered, with cancellation and rejection as terminal branches.
  • Notifications to the customer, the restaurant and the partner at every step; ratings after delivery.

Non-functional and constraints

  • A courier is never assigned two orders at once, no matter how many dispatchers run.
  • An order never sits waiting on a courier who closed the app: every offer expires.
  • A cancelled order never leaves a courier believing they are still delivering it.
  • In-memory and single-process; the clock and ID generators are injected so every test is deterministic.

Out of scope: real-time GPS tracking on a map, courier batching, surge pricing, restaurant onboarding, and the search index behind "browse".

Clarifying questions and assumptions

Question to ask Assumption taken here
When is the courier assigned — at order time or when the food is ready? While it cooks. Dispatch starts the moment the restaurant accepts, so the courier arrives as the food does. That is the DoorDash answer and it changes the state machine.
Can the customer cancel after a courier accepts? Yes, until the food is picked up. That is what makes the cancel-versus-accept race real rather than theoretical.
Do we charge at checkout or at delivery? Authorize at checkout, capture on delivery. Two calls, because food can fail to arrive.
What happens if a courier ignores the offer? It expires after 30 seconds and cascades to the next candidate, who is never the one who just passed.
Can a cart mix restaurants? No. One cart, one kitchen — the validation lives in Cart.add.
Are menu prices allowed to move between browsing and checkout? Yes, so the order stores an OrderItem price snapshot. The customer pays what they saw.
How is "nearby" computed? Straight-line distance inside one city. Swapping in a geospatial index changes one strategy class, nothing else.

Core entities and relationships

  • Restaurant owns one Menu of MenuItems, each with a price and an availability flag. One restaurant, many menu items.
  • Cart holds item ids and quantities for exactly one restaurant. It stores no prices — that is the point.
  • Order owns an immutable tuple of OrderItem price snapshots, a delivery Location, a fee, a discount and an OrderStatus. One order, one restaurant, at most one courier.
  • DeliveryPartner — id, live location, rating, PartnerStatus, and the order they currently hold.
  • DeliveryOffer — the lease: order, partner, created_at, expires_at, OfferStatus. At most one live offer names a given courier.
  • OrderService owns the order registry and every transition; DeliveryService owns courier state and offers; PaymentService owns authorizations.
  • FoodDeliveryService is the facade that sequences those three and publishes to the EventBus, which NotificationService observes.
  • AssignmentStrategy (NearestPartner, BestRatedNearby, FairRotation) and DiscountStrategy (FlatOff, PercentOff, FreeDelivery, NoDiscount) are the two policies you will be asked to change.

Multiplicities: restaurant 1 → * menu items, order 1 → * order items, order 1 → * offers over its life but 0..1 live at a time, courier 1 → 0..1 live offer, order 0..1 → 1 courier.

Class diagram

Domain: what an order is made of, and who can carry it.

classDiagram
    class Location {
        +lat: float
        +lon: float
        +distance_km(other) float
    }
    class MenuItem {
        +id: str
        +price: Money
        +available: bool
    }
    class Menu {
        +add(item) MenuItem
        +item(item_id) MenuItem
    }
    class Restaurant {
        +id: str
        +is_open: bool
        +prep_minutes: int
    }
    class Cart {
        +customer_id: str
        +restaurant_id: str
        +add(restaurant_id, item_id, quantity)
        +remove(item_id)
    }
    class OrderItem {
        +item_id: str
        +unit_price: Money
        +quantity: int
        +line_total Money
    }
    class Order {
        +id: str
        +status: OrderStatus
        +partner_id: str
        +subtotal Money
        +total Money
        +can_move_to(target) bool
        +is_assignable() bool
    }
    class DeliveryPartner {
        +id: str
        +rating: float
        +status: PartnerStatus
        +current_order_id: str
        +is_free() bool
    }
    class DeliveryOffer {
        +id: str
        +expires_at: float
        +status: OfferStatus
        +is_live(now) bool
    }
    class Payment {
        +amount: Money
        +status: PaymentStatus
    }
    class Rating {
        +stars: int
        +comment: str
    }
    Restaurant "1" *-- "1" Menu
    Menu "1" *-- "many" MenuItem
    Restaurant --> Location
    Cart ..> Restaurant : one kitchen only
    Order "1" *-- "many" OrderItem
    Order --> Location : deliver_to
    Order --> "0..1" DeliveryPartner
    DeliveryOffer --> Order
    DeliveryOffer --> DeliveryPartner : leases
    Order "1" o-- "1" Payment
    Order ..> Rating
    DeliveryPartner --> Location

Services and policies: three owners of state, one facade, two pluggable rules.

classDiagram
    class FoodDeliveryService {
        +place_order(cart, deliver_to, method, coupon_code) Order
        +restaurant_accepts(order_id) DeliveryOffer
        +dispatch(order_id) DeliveryOffer
        +partner_accepts(offer_id, partner_id) Order
        +sweep_offers() List~DeliveryOffer~
        +cancel_order(order_id) Order
        +deliver(order_id) Order
    }
    class OrderService {
        +place(cart, deliver_to, coupon_code) Order
        +transition(order_id, target) Order
        +attach_partner(order_id, partner_id) Order
    }
    class DeliveryService {
        +offer(order_id, origin) DeliveryOffer
        +accept(offer_id, partner_id) DeliveryOffer
        +decline(offer_id, partner_id) DeliveryOffer
        +sweep() List~DeliveryOffer~
        +release(offer)
        +void_order(order_id)
    }
    class PaymentService {
        +authorize(order, method) Payment
        +capture(order_id) Payment
        +void(order_id) Payment
    }
    class EventBus {
        +subscribe(topic, handler)
        +publish(event) int
        +failures() List~str~
    }
    class NotificationService {
        +inbox(recipient) List~str~
    }
    class AssignmentStrategy {
        <<interface>>
        +rank(origin, candidates) List~DeliveryPartner~
    }
    class NearestPartner
    class BestRatedNearby
    class FairRotation
    class DiscountStrategy {
        <<interface>>
        +discount(subtotal, delivery_fee) Money
    }
    class NoDiscount
    class FlatOff
    class PercentOff
    class FreeDelivery
    class CouponBook {
        +lookup(code) DiscountStrategy
    }
    class PaymentGateway {
        <<interface>>
        +authorize(amount) bool
        +capture(amount) bool
    }
    class GatewayFactory {
        +for_method(method) PaymentGateway
    }
    FoodDeliveryService --> OrderService
    FoodDeliveryService --> DeliveryService
    FoodDeliveryService --> PaymentService
    FoodDeliveryService --> EventBus
    EventBus --> NotificationService : notifies
    DeliveryService --> AssignmentStrategy
    AssignmentStrategy <|.. NearestPartner
    AssignmentStrategy <|.. BestRatedNearby
    AssignmentStrategy <|.. FairRotation
    OrderService --> CouponBook
    CouponBook --> DiscountStrategy
    DiscountStrategy <|.. NoDiscount
    DiscountStrategy <|.. FlatOff
    DiscountStrategy <|.. PercentOff
    DiscountStrategy <|.. FreeDelivery
    PaymentService --> GatewayFactory
    GatewayFactory ..> PaymentGateway : creates

Design patterns applied

Pattern Where Why it earns its place
State (as a table) ORDER_TRANSITIONS plus OrderService.transition Eight statuses and four actors. One dict is the whole machine, every service asks it, and the state diagram below is literally a drawing of that dict. Full State classes would be eight files with no behaviour in them.
Strategy AssignmentStrategy, DiscountStrategy The two rules the interviewer will change: "now rank by rating", "now add free delivery over 500". A strategy only ranks or computes — never mutates, never locks — so swapping one cannot introduce a race.
Event Bus + Observer EventBus, NotificationService OrderService must not import a push-notification client. It publishes order.assigned; whoever cares subscribes. Handlers run outside the bus lock and a failing handler is logged, not propagated: an order does not roll back because a push timed out.
Facade FoodDeliveryService The API layer gets eight verbs and never touches a lock. It is also the only place that knows the order of the three services.
Factory Method GatewayFactory.for_method Checkout carries the string "wallet". A registry maps it to a gateway; cash-on-delivery is a class, not an if.
Null Object NoDiscount CouponBook.lookup(None) returns an object, so pricing has no if coupon is None branch. An unknown code still raises — absent and invalid are different things.
Dependency Injection Clock, IdGenerator, strategies, gateways Tests freeze the clock at 2026-02-25, advance it 31 seconds and watch an offer expire, with no sleeps anywhere.

What was deliberately not used: Command for the dispatch cascade, and a Repository per entity. Command would let you queue and retry offers, and you should mention it as the natural next step when dispatch moves onto a worker pool — but here the cascade is three lines inside sweep_offers, and wrapping it in objects buys nothing today. Repositories are the persistence seam; say "each service holds a dict and would hold a repository protocol instead", then move on.

Key flows

The happy path: three actors, three services, one facade sequencing them.

sequenceDiagram
    autonumber
    actor U as Customer
    participant F as FoodDeliveryService
    participant O as OrderService
    participant P as PaymentService
    participant D as DeliveryService
    actor R as Restaurant
    actor C as Courier
    U->>F: place_order(cart, address, CARD, coupon)
    F->>O: place(cart) snapshots prices and discount
    O-->>F: Order(PLACED)
    F->>P: authorize(total)
    P-->>F: Payment(AUTHORIZED)
    F-)U: order.placed
    R->>F: restaurant_accepts(order_id)
    F->>O: PLACED to ACCEPTED to PREPARING
    F->>D: offer(order_id, kitchen_location)
    D->>D: rank idle couriers, lease the best
    D-->>C: DeliveryOffer valid for 30 s
    C->>F: partner_accepts(offer_id)
    F->>D: accept claims the lease
    F->>O: attach_partner
    R->>F: mark_ready
    C->>F: pick_up then deliver
    F->>P: capture(total)
    F->>D: complete(partner_id)
    F-)U: order.delivered

The two races. Left: the lease times out and cascades. Right: a cancel lands while a courier accepts.

sequenceDiagram
    autonumber
    participant F as FoodDeliveryService
    participant D as DeliveryService
    participant O as OrderService
    actor C1 as Courier one
    actor C2 as Courier two
    actor U as Customer
    F->>D: offer(order, kitchen)
    D-->>C1: offer expires at t plus 30 s
    Note over C1: silence
    F->>D: sweep() at t plus 31 s
    D->>D: offer EXPIRED, courier one back to IDLE and remembered as passed
    F->>D: offer(order, kitchen) again
    D-->>C2: new offer, never to courier one
    par courier two accepts
        C2->>F: partner_accepts(offer_id)
        F->>D: accept claims the lease
    and customer cancels
        U->>F: cancel_order(order_id)
        F->>O: transition to CANCELLED
    end
    F->>O: attach_partner
    alt order still assignable
        O-->>F: Order with courier two attached
    else already CANCELLED
        O-->>F: OrderStateError
        F->>D: release(offer) so courier two goes back to IDLE
    end

Order lifecycle. This diagram is ORDER_TRANSITIONS drawn out; the table in models.py is the only thing that enforces it.

stateDiagram-v2
    [*] --> PLACED : payment authorized
    PLACED --> ACCEPTED : restaurant accepts
    PLACED --> REJECTED : restaurant refuses, authorization voided
    PLACED --> CANCELLED : customer pulls out
    ACCEPTED --> PREPARING : cooking starts, dispatch begins
    ACCEPTED --> CANCELLED : customer pulls out
    PREPARING --> READY : food on the counter
    PREPARING --> CANCELLED : customer pulls out
    READY --> PICKED_UP : courier collects
    PICKED_UP --> DELIVERED : payment captured
    DELIVERED --> [*]
    CANCELLED --> [*]
    REJECTED --> [*]

Offer lifecycle. Only PENDING is live, and only one caller can move an offer out of it — that single transition is what makes double assignment impossible.

stateDiagram-v2
    [*] --> PENDING : courier leased, timer starts
    PENDING --> ACCEPTED : courier taps accept first
    PENDING --> DECLINED : courier passes, cascade to the next
    PENDING --> EXPIRED : 30 s elapse, sweep releases the courier
    PENDING --> VOIDED : order cancelled underneath
    ACCEPTED --> VOIDED : order cancelled after the claim
    ACCEPTED --> [*]
    DECLINED --> [*]
    EXPIRED --> [*]
    VOIDED --> [*]

Implementation

The order in the room: the vocabulary and the transition table first, because everything else asks it questions; then the catalog and the order; then the two policies; then the three services and the facade.

The enums plus two frozen sets are the state machine. Putting ORDER_TRANSITIONS in models.py rather than inside a service is deliberate — it is a business rule, and three different callers need the same answer:

code/lld/food_delivery/models.py — statuses and the transition table
class OrderStatus(StrEnum):
    PLACED = "placed"  # paid for (authorized), waiting for the restaurant
    ACCEPTED = "accepted"
    PREPARING = "preparing"
    READY = "ready"  # on the counter, waiting for a courier
    PICKED_UP = "picked_up"
    DELIVERED = "delivered"
    CANCELLED = "cancelled"  # customer pulled out
    REJECTED = "rejected"  # restaurant refused


class PartnerStatus(StrEnum):
    OFFLINE = "offline"
    IDLE = "idle"  # available for an offer
    OFFERED = "offered"  # holds one live lease and cannot be offered again
    DELIVERING = "delivering"


class OfferStatus(StrEnum):
    PENDING = "pending"
    ACCEPTED = "accepted"
    DECLINED = "declined"
    EXPIRED = "expired"  # the lease timed out
    VOIDED = "voided"  # the order disappeared under the courier


class PaymentStatus(StrEnum):
    AUTHORIZED = "authorized"
    CAPTURED = "captured"
    VOIDED = "voided"
    REFUNDED = "refunded"


class PaymentMethod(StrEnum):
    CARD = "card"
    WALLET = "wallet"
    CASH = "cash"


#: The single source of truth for the order lifecycle. Every service asks this
#: table rather than writing its own if/elif ladder, and the state diagram on the
#: page is a drawing of exactly this dict.
ORDER_TRANSITIONS: dict[OrderStatus, frozenset[OrderStatus]] = {
    OrderStatus.PLACED: frozenset({OrderStatus.ACCEPTED, OrderStatus.REJECTED, OrderStatus.CANCELLED}),
    OrderStatus.ACCEPTED: frozenset({OrderStatus.PREPARING, OrderStatus.CANCELLED}),
    OrderStatus.PREPARING: frozenset({OrderStatus.READY, OrderStatus.CANCELLED}),
    OrderStatus.READY: frozenset({OrderStatus.PICKED_UP}),
    OrderStatus.PICKED_UP: frozenset({OrderStatus.DELIVERED}),
    OrderStatus.DELIVERED: frozenset(),
    OrderStatus.CANCELLED: frozenset(),
    OrderStatus.REJECTED: frozenset(),
}

#: A courier may be attached only while the food still needs moving. Dispatch
#: starts at ACCEPTED on purpose: the courier should arrive as the food does.
ASSIGNABLE_STATUSES = frozenset({OrderStatus.ACCEPTED, OrderStatus.PREPARING, OrderStatus.READY})
code/lld/food_delivery/models.py — errors
class RestaurantClosedError(ConflictError):
    """The restaurant is not taking orders right now."""


class ItemUnavailableError(ConflictError):
    """An item in the cart went off the menu between browsing and checkout."""


class OrderStateError(InvalidStateError):
    """The order is not in a state that allows this transition."""


class OfferStateError(InvalidStateError):
    """The offer is expired, already answered, or belongs to a different courier."""


class NoPartnerAvailableError(ConflictError):
    """Nobody is idle within range who has not already passed on this order."""


class PaymentDeclinedError(ConflictError):
    """The gateway refused the authorization."""


class UnknownOrderError(NotFoundError):
    """No order with that id."""


class UnknownPartnerError(NotFoundError):
    """No delivery partner with that id."""

The catalog is unremarkable except for one line: Cart stores item ids and quantities and no prices at all. Prices are resolved once, at checkout, into immutable OrderItems:

code/lld/food_delivery/models.py — catalog and cart
@dataclass(slots=True)
class MenuItem:
    id: str
    name: str
    price: Money
    available: bool = True


@dataclass(slots=True)
class Menu:
    items: dict[str, MenuItem] = field(default_factory=dict)

    def add(self, item: MenuItem) -> MenuItem:
        self.items[item.id] = item
        return item

    def item(self, item_id: str) -> MenuItem:
        try:
            return self.items[item_id]
        except KeyError:
            raise NotFoundError(f"no menu item {item_id}") from None


@dataclass(slots=True)
class Restaurant:
    id: str
    name: str
    location: Location
    prep_minutes: int = 20
    is_open: bool = True
    menu: Menu = field(default_factory=Menu)


@dataclass(slots=True)
class Cart:
    """Item ids and quantities only. Prices are resolved at checkout, never here."""

    customer_id: str
    restaurant_id: str | None = None
    lines: dict[str, int] = field(default_factory=dict)

    def add(self, restaurant_id: str, item_id: str, quantity: int = 1) -> None:
        if quantity <= 0:
            raise ValidationError("quantity must be positive")
        if self.restaurant_id is not None and self.restaurant_id != restaurant_id:
            raise ValidationError("a cart holds items from one restaurant only")
        self.restaurant_id = restaurant_id
        self.lines[item_id] = self.lines.get(item_id, 0) + quantity

    def remove(self, item_id: str) -> None:
        self.lines.pop(item_id, None)
        if not self.lines:
            self.restaurant_id = None

    def is_empty(self) -> bool:
        return not self.lines

DeliveryOffer.is_live is the smallest method on the page and the most important one: an offer is usable only while it is PENDING and inside its window.

code/lld/food_delivery/models.py — order, courier, offer
@dataclass(frozen=True, slots=True)
class OrderItem:
    """A price snapshot. The menu may change tomorrow; this line never does."""

    item_id: str
    name: str
    unit_price: Money
    quantity: int

    @property
    def line_total(self) -> Money:
        return self.unit_price * self.quantity


@dataclass(slots=True)
class Order:
    id: str
    customer_id: str
    restaurant_id: str
    items: tuple[OrderItem, ...]
    deliver_to: Location
    delivery_fee: Money
    discount: Money
    placed_at: float
    status: OrderStatus = OrderStatus.PLACED
    partner_id: str | None = None
    coupon_code: str | None = None

    @property
    def subtotal(self) -> Money:
        total = Money(0)
        for item in self.items:
            total = total + item.line_total
        return total

    @property
    def total(self) -> Money:
        return self.subtotal + self.delivery_fee - self.discount

    def can_move_to(self, target: OrderStatus) -> bool:
        return target in ORDER_TRANSITIONS[self.status]

    def is_assignable(self) -> bool:
        return self.status in ASSIGNABLE_STATUSES


@dataclass(slots=True)
class DeliveryPartner:
    id: str
    name: str
    location: Location
    rating: float = 5.0
    ratings_count: int = 0
    status: PartnerStatus = PartnerStatus.OFFLINE
    deliveries_today: int = 0
    current_order_id: str | None = None

    def is_free(self) -> bool:
        return self.status is PartnerStatus.IDLE


@dataclass(slots=True)
class DeliveryOffer:
    """A time-boxed lease on one courier. Exactly one can be live per courier."""

    id: str
    order_id: str
    partner_id: str
    created_at: float
    expires_at: float
    status: OfferStatus = OfferStatus.PENDING

    def is_live(self, now: float) -> bool:
        return self.status is OfferStatus.PENDING and now < self.expires_at

Assignment strategies rank and nothing else. Notice that none of them touches a lock or mutates a courier — that is enforced by giving them a read-only Sequence:

code/lld/food_delivery/strategies.py — courier ranking
class AssignmentStrategy(Protocol):
    """Ranks candidate couriers for one order, best first.

    A strategy only *ranks*. It never mutates a courier and never takes a lock --
    ``DeliveryService`` does both, which is why swapping the ranking rule cannot
    introduce a race.
    """

    def rank(self, origin: Location, candidates: Sequence[DeliveryPartner]) -> list[DeliveryPartner]: ...


class NearestPartner:
    """Straight-line distance from the restaurant. The obvious first answer."""

    def rank(self, origin: Location, candidates: Sequence[DeliveryPartner]) -> list[DeliveryPartner]:
        return sorted(candidates, key=lambda p: (origin.distance_km(p.location), p.id))


class BestRatedNearby:
    """Everyone inside the radius, best rated first. Distance only breaks ties."""

    def __init__(self, radius_km: float = 3.0) -> None:
        self._radius = radius_km

    def rank(self, origin: Location, candidates: Sequence[DeliveryPartner]) -> list[DeliveryPartner]:
        nearby = [p for p in candidates if origin.distance_km(p.location) <= self._radius]
        return sorted(nearby, key=lambda p: (-p.rating, origin.distance_km(p.location), p.id))


class FairRotation:
    """Fewest deliveries today first, then distance. Keeps earnings even."""

    def __init__(self, radius_km: float = 5.0) -> None:
        self._radius = radius_km

    def rank(self, origin: Location, candidates: Sequence[DeliveryPartner]) -> list[DeliveryPartner]:
        nearby = [p for p in candidates if origin.distance_km(p.location) <= self._radius]
        return sorted(nearby, key=lambda p: (p.deliveries_today, origin.distance_km(p.location), p.id))
code/lld/food_delivery/strategies.py — coupons, with a Null Object default
class DiscountStrategy(Protocol):
    """How much comes off this order. Implementations are stateless."""

    def discount(self, subtotal: Money, delivery_fee: Money) -> Money: ...


class NoDiscount:
    """Null Object: the default coupon. Callers never branch on ``coupon is None``."""

    def discount(self, subtotal: Money, delivery_fee: Money) -> Money:
        return Money(0)


class FlatOff:
    """A fixed amount off, above a minimum spend."""

    def __init__(self, amount: Money, min_subtotal: Money) -> None:
        self._amount = amount
        self._min = min_subtotal

    def discount(self, subtotal: Money, delivery_fee: Money) -> Money:
        if subtotal < self._min:
            return Money(0)
        return min(self._amount, subtotal)


class PercentOff:
    """A percentage off the food, capped so a large order cannot drain the budget."""

    def __init__(self, percent: Decimal, cap: Money) -> None:
        if not 0 < percent < 1:
            raise ValidationError("percent must be a fraction between 0 and 1")
        self._percent = percent
        self._cap = cap

    def discount(self, subtotal: Money, delivery_fee: Money) -> Money:
        return min(subtotal * self._percent, self._cap)


class FreeDelivery:
    """Takes the delivery fee off instead of the food."""

    def discount(self, subtotal: Money, delivery_fee: Money) -> Money:
        return delivery_fee


class CouponBook:
    """Code to strategy. An unknown code is an error; *no* code is a Null Object."""

    def __init__(self, coupons: dict[str, DiscountStrategy] | None = None) -> None:
        self._coupons: dict[str, DiscountStrategy] = dict(coupons or {})

    def register(self, code: str, strategy: DiscountStrategy) -> None:
        self._coupons[code.upper()] = strategy

    def lookup(self, code: str | None) -> DiscountStrategy:
        if code is None:
            return NoDiscount()
        try:
            return self._coupons[code.upper()]
        except KeyError:
            raise ValidationError(f"unknown coupon {code!r}") from None

OrderService owns one lock and every transition. transition raises rather than returning False: a caller that tried an illegal move has a bug, and silence would hide it.

code/lld/food_delivery/services.py — OrderService
class OrderService:
    """Owns the order registry and *every* status change.

    One lock guards both. Each transition is a check-and-flip against
    ``ORDER_TRANSITIONS``: a caller can never observe an order half-way between
    two states, and two callers can never both win the same transition.
    """

    def __init__(
        self,
        restaurants: Iterable[Restaurant],
        clock: Clock | None = None,
        ids: IdGenerator | None = None,
        coupons: CouponBook | None = None,
        delivery_fee: Money = DEFAULT_DELIVERY_FEE,
    ) -> None:
        self._restaurants = {r.id: r for r in restaurants}
        self._clock = clock or SystemClock()
        self._ids = ids or SequentialIdGenerator("O")
        self._coupons = coupons or CouponBook()
        self._fee = delivery_fee
        self._orders: dict[str, Order] = {}
        self._lock = threading.Lock()

    def restaurant(self, restaurant_id: str) -> Restaurant:
        try:
            return self._restaurants[restaurant_id]
        except KeyError:
            raise UnknownOrderError(f"no restaurant {restaurant_id}") from None

    def nearby(self, location: Location, radius_km: float = 5.0) -> list[Restaurant]:
        open_ones = [r for r in self._restaurants.values() if r.is_open]
        within = [r for r in open_ones if location.distance_km(r.location) <= radius_km]
        return sorted(within, key=lambda r: (location.distance_km(r.location), r.id))

    def order(self, order_id: str) -> Order:
        with self._lock:
            try:
                return self._orders[order_id]
            except KeyError:
                raise UnknownOrderError(f"unknown order {order_id}") from None

    def place(self, cart: Cart, deliver_to: Location, coupon_code: str | None = None) -> Order:
        """Snapshot prices *now*: the menu may change while the food is cooking."""
        if cart.is_empty() or cart.restaurant_id is None:
            raise ValidationError("cannot place an empty cart")
        restaurant = self.restaurant(cart.restaurant_id)
        if not restaurant.is_open:
            raise RestaurantClosedError(f"{restaurant.name} is closed")
        items = []
        for item_id, quantity in cart.lines.items():
            menu_item = restaurant.menu.item(item_id)
            if not menu_item.available:
                raise ItemUnavailableError(f"{menu_item.name} is sold out")
            items.append(OrderItem(item_id, menu_item.name, menu_item.price, quantity))
        order = Order(
            id=self._ids.next_id(),
            customer_id=cart.customer_id,
            restaurant_id=restaurant.id,
            items=tuple(items),
            deliver_to=deliver_to,
            delivery_fee=self._fee,
            discount=Money(0),
            placed_at=self._clock.now(),
            coupon_code=coupon_code,
        )
        order.discount = self._coupons.lookup(coupon_code).discount(order.subtotal, self._fee)
        with self._lock:
            self._orders[order.id] = order
        return order

    def transition(self, order_id: str, target: OrderStatus) -> Order:
        """The only way an order changes state. Rejected moves raise, never no-op."""
        with self._lock:
            order = self._orders.get(order_id)
            if order is None:
                raise UnknownOrderError(f"unknown order {order_id}")
            if not order.can_move_to(target):
                raise OrderStateError(f"order {order_id} cannot move {order.status} to {target}")
            order.status = target
            return order

    def attach_partner(self, order_id: str, partner_id: str) -> Order:
        """Pin a courier. Fails if the order was cancelled or already has one."""
        with self._lock:
            order = self._orders.get(order_id)
            if order is None:
                raise UnknownOrderError(f"unknown order {order_id}")
            if not order.is_assignable():
                raise OrderStateError(f"order {order_id} is {order.status}, not assignable")
            if order.partner_id is not None:
                raise OrderStateError(f"order {order_id} already has courier {order.partner_id}")
            order.partner_id = partner_id
            return order

DeliveryService is where the lease lives. offer flips a courier from IDLE to OFFERED inside the lock, which is the entire double-assignment defence; _retire is the one place a courier is freed, so the three ways an offer can die share their cleanup:

code/lld/food_delivery/services.py — DeliveryService and the lease
class DeliveryService:
    """Courier state, live offers and the lease. One lock over all three.

    The invariant: a courier is in exactly one of IDLE, OFFERED and DELIVERING,
    and at most one PENDING offer names them. ``offer`` flips IDLE to OFFERED
    inside the lock, so a courier that two dispatchers pick simultaneously is
    still only offered one order -- the second dispatcher finds nobody idle and
    moves down its ranking.
    """

    OFFER_TIMEOUT_SECONDS = 30.0

    def __init__(
        self,
        partners: Iterable[DeliveryPartner],
        strategy: AssignmentStrategy | None = None,
        clock: Clock | None = None,
        ids: IdGenerator | None = None,
        offer_timeout: float = OFFER_TIMEOUT_SECONDS,
    ) -> None:
        self._partners = {p.id: p for p in partners}
        self._strategy = strategy or NearestPartner()
        self._clock = clock or SystemClock()
        self._ids = ids or SequentialIdGenerator("OF")
        self._timeout = offer_timeout
        self._offers: dict[str, DeliveryOffer] = {}
        self._passed_on: dict[str, set[str]] = {}
        self._lock = threading.Lock()

    def partner(self, partner_id: str) -> DeliveryPartner:
        try:
            return self._partners[partner_id]
        except KeyError:
            raise UnknownPartnerError(f"no partner {partner_id}") from None

    def go_online(self, partner_id: str, location: Location | None = None) -> DeliveryPartner:
        with self._lock:
            partner = self.partner(partner_id)
            if location is not None:
                partner.location = location
            partner.status = PartnerStatus.IDLE
            return partner

    def update_location(self, partner_id: str, location: Location) -> None:
        with self._lock:
            self.partner(partner_id).location = location

    def offer(self, order_id: str, origin: Location) -> DeliveryOffer:
        """Lease the best idle courier who has not already passed on this order."""
        with self._lock:
            now = self._clock.now()
            live = [o for o in self._offers.values() if o.order_id == order_id and o.is_live(now)]
            if live:
                return live[0]  # dispatch is idempotent: one live offer per order
            passed = self._passed_on.setdefault(order_id, set())
            free = [p for p in self._partners.values() if p.is_free() and p.id not in passed]
            ranked = self._strategy.rank(origin, free)
            if not ranked:
                raise NoPartnerAvailableError(f"no courier free for order {order_id}")
            partner = ranked[0]
            offer = DeliveryOffer(self._ids.next_id(), order_id, partner.id, now, now + self._timeout)
            partner.status = PartnerStatus.OFFERED
            partner.current_order_id = order_id
            self._offers[offer.id] = offer
            return offer

    def accept(self, offer_id: str, partner_id: str) -> DeliveryOffer:
        """Claim the lease. Exactly one caller can move an offer out of PENDING."""
        with self._lock:
            offer = self._offers.get(offer_id)
            if offer is None:
                raise OfferStateError(f"unknown offer {offer_id}")
            if offer.partner_id != partner_id:
                raise OfferStateError(f"offer {offer_id} belongs to {offer.partner_id}")
            if not offer.is_live(self._clock.now()):
                raise OfferStateError(f"offer {offer_id} is {offer.status} or expired")
            offer.status = OfferStatus.ACCEPTED
            partner = self.partner(partner_id)
            partner.status = PartnerStatus.DELIVERING
            return offer

    def decline(self, offer_id: str, partner_id: str) -> DeliveryOffer:
        with self._lock:
            offer = self._offers.get(offer_id)
            if offer is None or offer.partner_id != partner_id:
                raise OfferStateError(f"offer {offer_id} is not addressed to {partner_id}")
            if offer.status is not OfferStatus.PENDING:
                raise OfferStateError(f"offer {offer_id} is already {offer.status}")
            self._retire(offer, OfferStatus.DECLINED)
            return offer

    def sweep(self) -> list[DeliveryOffer]:
        """Expire every lease whose timeout has passed. Call it from a timer."""
        now = self._clock.now()
        with self._lock:
            stale = [o for o in self._offers.values() if o.status is OfferStatus.PENDING and now >= o.expires_at]
            for offer in stale:
                self._retire(offer, OfferStatus.EXPIRED)
            return stale

    def release(self, offer: DeliveryOffer) -> None:
        """Undo an accepted claim because the order moved under the courier."""
        with self._lock:
            self._retire(offer, OfferStatus.VOIDED, remember=False)

    def void_order(self, order_id: str) -> None:
        with self._lock:
            for offer in self._offers.values():
                if offer.order_id == order_id and offer.status in (OfferStatus.PENDING, OfferStatus.ACCEPTED):
                    self._retire(offer, OfferStatus.VOIDED, remember=False)
            self._passed_on.pop(order_id, None)

    def complete(self, partner_id: str) -> DeliveryPartner:
        with self._lock:
            partner = self.partner(partner_id)
            partner.status = PartnerStatus.IDLE
            partner.current_order_id = None
            partner.deliveries_today += 1
            return partner

    def offers_for(self, order_id: str) -> list[DeliveryOffer]:
        with self._lock:
            return [o for o in self._offers.values() if o.order_id == order_id]

    def _retire(self, offer: DeliveryOffer, status: OfferStatus, remember: bool = True) -> None:
        """Caller holds the lock. Frees the courier and optionally records the pass."""
        offer.status = status
        partner = self._partners.get(offer.partner_id)
        # Free the courier only if they are still holding *this* order. Retiring a
        # stale offer must never take a courier off the delivery they have since
        # accepted -- the classic "release a resource someone else now owns" bug.
        still_held = partner is not None and partner.current_order_id == offer.order_id
        if still_held and partner is not None and partner.status is not PartnerStatus.OFFLINE:
            partner.status = PartnerStatus.IDLE
            partner.current_order_id = None
        if remember:
            self._passed_on.setdefault(offer.order_id, set()).add(offer.partner_id)

The bus is deliberately dull: subscribers under the lock, handlers outside it, failures recorded rather than raised.

code/lld/food_delivery/messaging.py — EventBus
class EventBus:
    """Topic to handlers, dispatched synchronously with per-handler error isolation.

    The lock only guards the subscriber map and the failure log; handlers run
    *outside* it, so a slow notification cannot stall the order that published it.
    A failing handler is recorded and the next one still runs -- an order must not
    roll back because a push notification timed out.
    """

    def __init__(self) -> None:
        self._lock = threading.Lock()
        self._handlers: dict[str, list[Handler]] = {}
        self._failures: list[tuple[str, str]] = []

    def subscribe(self, topic: str, handler: Handler) -> None:
        with self._lock:
            self._handlers.setdefault(topic, []).append(handler)

    def publish(self, event: Event) -> int:
        with self._lock:
            handlers = list(self._handlers.get(event.topic, ()))
        delivered = 0
        for handler in handlers:
            try:
                handler(event)
                delivered += 1
            except Exception as exc:  # isolation is the whole point of a bus
                with self._lock:
                    self._failures.append((event.topic, str(exc)))
        return delivered

    def failures(self) -> list[tuple[str, str]]:
        with self._lock:
            return list(self._failures)

The facade sequences and never computes. Every cross-service step is claim, act, revert on failure — partner_accepts is the canonical example:

code/lld/food_delivery/facade.py — FoodDeliveryService
class FoodDeliveryService:
    """The one object the API layer holds. It sequences, it does not compute.

    Every step is *claim, then act, then revert on failure*: the offer lease is
    claimed in ``DeliveryService`` and the order is pinned in ``OrderService``,
    and if the second call fails the first is undone. No method ever holds two
    service locks at once, so there is no lock order to get wrong.
    """

    def __init__(
        self,
        restaurants: Iterable[Restaurant],
        partners: Iterable[DeliveryPartner],
        strategy: AssignmentStrategy | None = None,
        coupons: CouponBook | None = None,
        clock: Clock | None = None,
        ids: IdGenerator | None = None,
        offer_timeout: float = DeliveryService.OFFER_TIMEOUT_SECONDS,
    ) -> None:
        clock = clock or SystemClock()
        self._clock = clock
        self.bus = EventBus()
        self.notifications = NotificationService(self.bus)
        self.orders = OrderService(restaurants, clock=clock, ids=ids or SequentialIdGenerator("O"), coupons=coupons)
        self.delivery = DeliveryService(partners, strategy, clock=clock, offer_timeout=offer_timeout)
        self.payments = PaymentService(clock=clock)
        self._ratings: list[Rating] = []
        self._lock = threading.Lock()

    # -- customer ---------------------------------------------------------------
    def browse(self, location: Location, radius_km: float = 5.0) -> list[Restaurant]:
        return self.orders.nearby(location, radius_km)

    def place_order(
        self, cart: Cart, deliver_to: Location, method: PaymentMethod, coupon_code: str | None = None
    ) -> Order:
        order = self.orders.place(cart, deliver_to, coupon_code)
        try:
            self.payments.authorize(order, method)
        except PaymentDeclinedError:
            self.orders.transition(order.id, OrderStatus.CANCELLED)
            raise
        self._publish("order.placed", order)
        return order

    def cancel_order(self, order_id: str) -> Order:
        """Flip the order first: a courier accepting at the same instant now loses."""
        order = self.orders.transition(order_id, OrderStatus.CANCELLED)
        self.delivery.void_order(order_id)
        self.payments.void(order_id)
        self._publish("order.cancelled", order)
        return order

    def rate_delivery(self, order_id: str, stars: int, comment: str = "") -> Rating:
        order = self.orders.order(order_id)
        if order.status is not OrderStatus.DELIVERED or order.partner_id is None:
            raise OrderStateError(f"order {order_id} was not delivered")
        rating = Rating(order_id, order.partner_id, stars, comment)
        partner = self.delivery.partner(order.partner_id)
        with self._lock:
            self._ratings.append(rating)
            total = partner.rating * partner.ratings_count + stars
            partner.ratings_count += 1
            partner.rating = round(total / partner.ratings_count, 2)
        return rating

    # -- restaurant -------------------------------------------------------------
    def restaurant_accepts(self, order_id: str) -> DeliveryOffer | None:
        order = self.orders.transition(order_id, OrderStatus.ACCEPTED)
        self._publish("order.accepted", order)
        self.orders.transition(order_id, OrderStatus.PREPARING)
        return self.dispatch(order_id)  # dispatch while the food cooks

    def restaurant_rejects(self, order_id: str) -> Order:
        order = self.orders.transition(order_id, OrderStatus.REJECTED)
        self.payments.void(order_id)
        self._publish("order.rejected", order)
        return order

    def mark_ready(self, order_id: str) -> Order:
        order = self.orders.transition(order_id, OrderStatus.READY)
        self._publish("order.ready", order)
        return order

    # -- dispatch ---------------------------------------------------------------
    def dispatch(self, order_id: str) -> DeliveryOffer | None:
        """Offer the order to the next-best courier. None means nobody is left."""
        order = self.orders.order(order_id)
        if not order.is_assignable() or order.partner_id is not None:
            return None
        try:
            return self.delivery.offer(order_id, self.orders.restaurant(order.restaurant_id).location)
        except NoPartnerAvailableError:
            return None

    def partner_accepts(self, offer_id: str, partner_id: str) -> Order:
        offer = self.delivery.accept(offer_id, partner_id)  # claim the lease
        try:
            order = self.orders.attach_partner(offer.order_id, partner_id)
        except OrderStateError:
            self.delivery.release(offer)  # the order was cancelled under the courier
            raise
        self._publish("order.assigned", order)
        return order

    def partner_declines(self, offer_id: str, partner_id: str) -> DeliveryOffer | None:
        offer = self.delivery.decline(offer_id, partner_id)
        return self.dispatch(offer.order_id)

    def sweep_offers(self) -> list[DeliveryOffer]:
        """Expire stale leases and cascade each order to the next courier."""
        expired = self.delivery.sweep()
        for offer in expired:
            self.dispatch(offer.order_id)
        return expired

    def pick_up(self, order_id: str) -> Order:
        order = self.orders.order(order_id)
        if order.partner_id is None:
            raise OrderStateError(f"order {order_id} has no courier")
        order = self.orders.transition(order_id, OrderStatus.PICKED_UP)
        self._publish("order.picked_up", order)
        return order

    def deliver(self, order_id: str) -> Order:
        order = self.orders.transition(order_id, OrderStatus.DELIVERED)
        self.payments.capture(order_id)
        if order.partner_id:
            self.delivery.complete(order.partner_id)
        self._publish("order.delivered", order)
        return order

    def _publish(self, topic: str, order: Order) -> None:
        payload = {
            "order_id": order.id,
            "customer_id": order.customer_id,
            "restaurant": order.restaurant_id,
            "total": str(order.total),
        }
        if order.partner_id:
            payload["partner"] = order.partner_id
        self.bus.publish(Event(topic, self._clock.now(), payload))

Running python -m lld.food_delivery.demo walks a timeout, a decline and a delivery:

open near home: ['Curry Corner']
O-1 placed: subtotal 19.00 USD, SAVE10 -1.90 USD, fee 2.50 USD, total 19.60 USD
O-1 accepted and cooking; OF-1 offered to p1 until +30 s
OF-1 expired (expired); p1 is idle again
cascaded to p2, who declines
cascaded again to p3, who accepts
O-1 delivered by p3; payment captured 19.60 USD
p3 rating is now 4.84
    customer: we sent O-1 to r1
    customer: r1 is cooking O-1
    customer: p3 is bringing O-1
    customer: O-1 is on the counter
    customer: p3 picked up O-1
    customer: O-1 delivered by p3

Concurrency and edge cases

Which lock protects what. Three, and no method ever holds two of them:

  1. OrderService._lock guards the order registry and every status change. transition and attach_partner are check-and-flip: two callers cannot both move an order out of PREPARING, and a courier cannot be attached to an order that is no longer assignable.
  2. DeliveryService._lock guards courier statuses, the offer registry and the per-order set of couriers who already passed. offer reads the idle set, ranks it and flips the winner to OFFERED in one critical section.
  3. PaymentService._lock guards the payment registry, with the same check-and-flip on AUTHORIZED → CAPTURED | VOIDED. EventBus has a fourth lock over its subscriber map, but that map is written once at wiring time.

Why nesting is banned, and what replaces it. The facade needs two services to agree, so the temptation is to hold both locks. Instead every cross-service step is claim, act, revert: partner_accepts claims the lease in DeliveryService, then pins the courier in OrderService, and calls release if the second step fails. There is no lock order to get wrong because there are never two locks held, and the recovery path is one line you can point at on the board.

No double assignment. Two dispatchers running for two different orders can pick the same courier from the same idle list — but only one of them executes partner.status = OFFERED under the lock. The other finds nobody idle at that rank and walks down its own ranking, or reports that nobody is free. dispatch is also idempotent: if a live offer already exists for the order, offer returns it instead of leasing a second courier.

The lease timeout. A courier who closes their phone would otherwise pin an order forever. sweep expires every offer past expires_at, frees the courier, and records them in _passed_on[order_id] so the cascade never comes back to them. Call it from a timer; the test calls it directly after advancing a FakeClock by 31 seconds, which is why there are no sleeps in the suite.

Cancel versus accept. Both calls may legitimately succeed — a courier can accept a millisecond before the customer cancels. What must never happen is a courier left believing they are delivering a dead order. cancel_order flips the order first, then calls void_order, which retires any live or accepted offer and returns the courier to IDLE. So the post-condition the test asserts is not "exactly one wins" but "if the order is cancelled, the courier is idle" — which is the invariant that actually matters.

Sizing the locks. A city doing 500k orders a day is 500,000 / 86,400 ≈ 6 orders/s on average and roughly 18/s at a 3x peak. Each order takes the order lock about eight times over its life, so about 150 acquisitions per second; an uncontended lock costs about 17 ns, so the lock is four orders of magnitude away from mattering. Say that out loud before anyone proposes sharding a mutex.

Edge cases handled: an item selling out between browsing and checkout, an empty cart, a closed kitchen, a cart mixing two restaurants, a coupon below its minimum spend, an unknown coupon code versus no code at all, a declined authorization cancelling the order, a rejected order voiding the authorization, accepting an expired offer, accepting somebody else's offer, declining until the cascade is exhausted, and rating an order that was never delivered.

Common mistake

Assigning the courier by writing order.partner_id = partner.id and calling it done. That is a write, not a lease: nothing stops a second dispatcher writing over it, nothing expires when the courier goes silent, and nothing tells you who already declined. The offer object with an expiry is the entire difference between a toy and a dispatch system — and it is also the answer to "how would you scale this?", because a lease is exactly what you would put in Redis with a TTL.

Extensibility and follow-ups

  • ETA and surge: an EtaService composed from Restaurant.prep_minutes and courier distance. Surge is a DiscountStrategy with a negative discount, or better, a PricingStrategy sibling so the two do not fight over one field.
  • Batching two orders onto one courier: DeliveryPartner.current_order_id becomes current_order_ids: list[str] with a capacity, and is_free becomes has_capacity. Every strategy keeps working because it only reads.
  • Restaurant throttling: a Restaurant.max_open_orders check inside restaurant_accepts, or a Specification composed with the existing open-and-in-range filter.
  • Refund saga: a cancel after pickup cannot void an authorization, so it becomes capture-then-refund with compensating steps. PaymentService already models CAPTURED → REFUNDED; the saga is the ordering of those steps plus retries.
  • Dispatch on a worker pool: sweep_offers becomes a scheduled job and each cascade becomes a Command on a queue. That is also the moment the offer registry moves to Redis with a real TTL and the process stops being the source of truth.
  • Scale: at that point it is a system design question — geo-sharded dispatch, WebSocket courier location ingestion, and an outbox instead of an in-process bus. The HLD case study is the same problem with those constraints.

Interview tip

When you are asked "how do you assign a delivery partner?", do not start with the ranking algorithm — that is the easy half and it is a Strategy. Start with "an assignment is a lease with an expiry, and only one caller can move it out of PENDING". Ranking is a policy you can change in a class; the lease is the correctness argument, and it is what separates an SDE2 answer from an SDE1 one.

Tests

tests/test_food_delivery.py has 20 cases. The two concurrency tests are the ones to walk through. The first races eight dispatches against three couriers and asserts three distinct leases — and, just as importantly, that a lease is not an assignment:

code/lld/food_delivery/tests/test_food_delivery.py — no double assignment
def test_concurrent_dispatch_never_offers_one_courier_two_orders(clock: FakeClock) -> None:
    service = build(clock, couriers=3)
    orders = [service.place_order(full_cart(f"c{i}"), HOME, PaymentMethod.CARD) for i in range(8)]

    with ThreadPoolExecutor(max_workers=8) as pool:
        offers = [o for o in pool.map(lambda o: service.restaurant_accepts(o.id), orders) if o is not None]

    leased = [o.partner_id for o in offers]
    assert len(leased) == 3 and len(set(leased)) == 3  # three couriers, three leases, no overlap
    assert all(service.delivery.partner(p).status is PartnerStatus.OFFERED for p in leased)
    assert sum(1 for o in orders if o.partner_id is not None) == 0  # a lease is not an assignment


def test_only_one_thread_can_accept_the_same_offer(clock: FakeClock) -> None:
    service = build(clock, couriers=1)
    order = service.place_order(full_cart(), HOME, PaymentMethod.CARD)
    offer = service.restaurant_accepts(order.id)
    assert offer is not None

    def accept(_: int) -> bool:
        try:
            service.partner_accepts(offer.id, "p1")
        except OfferStateError:
            return False
        return True

    with ThreadPoolExecutor(max_workers=12) as pool:
        results = list(pool.map(accept, range(12)))
    assert results.count(True) == 1 and order.partner_id == "p1"

The second interleaves twenty accepts with twenty cancels and asserts the invariant that actually matters:

code/lld/food_delivery/tests/test_food_delivery.py — cancel versus accept
def test_cancel_racing_accept_never_strands_a_courier(clock: FakeClock) -> None:
    """Both calls may succeed; what must never happen is a courier left on a dead order."""
    service = build(clock, couriers=20)
    pairs = []
    for i in range(20):
        order = service.place_order(full_cart(f"c{i}"), HOME, PaymentMethod.CARD)
        offer = service.restaurant_accepts(order.id)
        assert offer is not None
        pairs.append((order, offer))

    def run(task: tuple) -> None:
        action, order, offer = task
        try:
            if action == "accept":
                service.partner_accepts(offer.id, offer.partner_id)
            else:
                service.cancel_order(order.id)
        except (OfferStateError, OrderStateError):
            pass

    tasks = [(a, order, offer) for order, offer in pairs for a in ("accept", "cancel")]
    with ThreadPoolExecutor(max_workers=16) as pool:
        list(pool.map(run, tasks))

    for order, offer in pairs:
        partner = service.delivery.partner(offer.partner_id)
        if order.status is OrderStatus.CANCELLED:
            assert partner.status is PartnerStatus.IDLE and partner.current_order_id is None
        else:
            assert order.partner_id == partner.id and partner.status is PartnerStatus.DELIVERING

The rest cover: price snapshotting and coupon application, with the menu changed afterwards to prove the snapshot holds; sold-out items, empty carts and closed kitchens; the transition table through a six-case parametrize of legal and illegal moves; the lease making a second order undispatchable and dispatch being idempotent; the 30-second timeout cascading to a courier who has not passed, and the stale offer being unusable; retiring a stale offer leaving a courier who has since accepted another order alone, because freeing a resource someone else now owns is the failure this design exists to avoid; declining until the cascade is exhausted; authorize-then-capture on the happy path and authorize-then-void on rejection; the three assignment strategies each picking a different courier from the same three candidates; and the bus isolating a handler that throws. Run them with uv run pytest code/lld/food_delivery -q.

45-minute pacing

Minutes What to do What to say or write
0–5 Clarify Three actors. When is the courier assigned? Can the customer cancel late? Authorize or capture? Out of scope: tracking, batching, surge.
5–10 State machine first Draw the eight order statuses and the arrows. Say "this is a table, not a class hierarchy", and write ORDER_TRANSITIONS.
10–18 Entities and class diagram Restaurant, Menu, Cart, Order + OrderItem snapshot, DeliveryPartner, DeliveryOffer. Then the three services and the facade over them.
18–34 Code OrderService.transition → DeliveryService.offer (the lease) → accept → FoodDeliveryService.partner_accepts with the revert. Say "claim, act, revert" as you write it.
34–40 Concurrency Three locks, never nested. The double-assignment argument, the timeout sweep, and why cancel-versus-accept is about the courier and not about who wins.
40–45 Extensions Batching, ETA, refund saga, then hand off to the HLD version: Redis leases with a TTL and geo-sharded dispatch.