System design template

Food delivery platform architecture.

Three sides to coordinate and one deadline that nobody controls: the kitchen. Dispatch quality matters more here than any amount of throughput.

Download for macOS
v0.1.33 · .dmg · Apple Silicon & Intel
Signed & notarized by Apple · opens without a Gatekeeper warning
sha256 698955a0187bc039f4c74f5d05a9f10fbb27376a45788a0a241d1326b73873c7
Download for Windows instead
$curl -fsSL https://lucidtrain.com/install.sh | sh
Food delivery platform architecture. 12 components across 4 tiers.
Food delivery platform architecture. Rendered by the same ELK layout engine the app runs: the agent emits components, tiers and edges, and the engine places them, so the boxes cannot overlap.

The components

Every row below is read from the graph that produced the diagram above, so the two cannot disagree.

ComponentTierWhy it is there
Customer appClientSupporting component
Courier appClientSupporting component
Merchant tabletClientSupporting component
API gatewayEdgeSupporting component
Menu serviceApplicationSupporting component
Order serviceApplicationSupporting component
DispatchApplicationAssigns couriers on prep-time estimates
ETA modelApplicationSupporting component
Live trackingApplicationSupporting component
PostgresDataSupporting component
Redis geoDataSupporting component
KafkaDataSupporting component

Design decisions worth arguing about

A diagram shows what was chosen. It does not show what it cost, and that is usually the part that matters in a review or an interview.

Dispatch on predicted prep time, not on order time

Sending a courier the moment an order is placed means they wait at the restaurant, which wastes the scarcest resource in the system. Sending them to arrive as the food is ready requires predicting prep time, which is a model that will be wrong. Being wrong early wastes courier time; being wrong late means cold food and a bad review. The asymmetry means the model should be deliberately biased toward arriving slightly early.

Three clients, one order state machine

Customer, courier and merchant each see a different projection of the same order, and each can advance it. Letting each app write its own state fields produces orders in impossible combinations. Keeping one state machine with defined transitions makes the invalid states unrepresentable, at the cost of an extra hop for every update and a service that everyone depends on.

The ETA is a product feature, not a calculation

A precise ETA that is sometimes wrong is worse for satisfaction than a padded one that is usually beaten. That means the number shown to the customer is deliberately not the model's best estimate, which feels wrong to engineers and is well supported by how people actually respond to a late delivery versus an early one.

Batch deliveries improve economics and degrade experience

Assigning two nearby orders to one courier meaningfully improves unit economics and makes at least one customer wait longer. Where that line sits is a business decision the dispatch service has to encode, and encoding it explicitly, with a maximum acceptable detour, is much better than letting it emerge from an optimisation objective nobody reads.

How it changes with scale

Demand is extremely peaked around meal times and highly local, so capacity is a per-city, per-hour problem rather than a global one. Courier supply, not compute, is the binding constraint, which means the highest-value engineering is usually in dispatch quality rather than in throughput.

Where it breaks first

Courier supply shortage during a peak. Orders accumulate faster than they can be assigned, ETAs stretch, customers cancel, and cancellations release food that has already been cooked. The system cannot fix this technically; it can only degrade honestly by widening ETAs and, at the extreme, refusing new orders in an area rather than accepting orders it cannot serve.

Draw this yourself

Open the Diagram tab and describe the system. The agent emits a semantic graph rather than coordinates, so you can edit the components and the layout re-solves instead of drifting.

shell
$ Diagram a food delivery platform: customer, courier and merchant apps behind an API gateway, menu and order services, a dispatch service using prep-time estimates and courier positions, an ETA model, live tracking, and Kafka driving the order lifecycle.

When the shape is right, Implement in code turns the canvas into a markdown specification, every component, every relationship and the notes, and starts a real turn in the Code tab with it.

FAQ

Questions about this design

How is live tracking delivered efficiently?

Positions go into a geospatial store on ingest, and customer apps poll or subscribe for the single courier assigned to them. Broadcasting all courier positions would be both wasteful and a privacy problem.

Should dispatch be synchronous with order placement?

No. Order acceptance should not wait for a courier to be found, or a dispatch delay becomes a checkout failure. Placing the order and dispatching asynchronously separates those failure modes.

How do you handle a merchant rejecting an order?

As a normal state transition with compensation: refund, notify, and release any assigned courier. Treating rejection as an error path rather than an expected transition is how orders get stuck.

Why event-driven rather than direct calls?

Because an order touches many services over tens of minutes, and an event log gives you a replayable history of what happened. When a customer asks why their order was late, that log is the only way to answer.

More templates