Back to Blog

Food Delivery App Architecture: How to Build for Scale from Day One

Max Bantsevich,  | dev.family
Max Bantsevich
CEO

Sep 2, 2026

25 min reading

Food Delivery App Architecture: How to Build for Scale from Day One - dev.family

By Max Bantsevich, CEO at dev.family

A food delivery startup handles 100 orders a day without any real trouble — order status updates in real time, the map shows exactly where the courier is, and menu availability stays accurate. Then the team runs a promotion and volume jumps to 800 orders in an afternoon, and everything that worked fine at 100 orders starts breaking: order states drift out of sync, courier locations lag three or four minutes behind reality, and two customers end up claiming the same dish that was actually already out of stock. The engineering team spends the weekend tracing all of it back to assumptions made in sprint one, when nobody was thinking that far ahead.

Food delivery app architecture is not the same problem as a standard e-commerce build. Three things make it fundamentally different: state changes happen in seconds, not hours; three concurrent user types — customer, courier, kitchen — need conflicting real-time views of the same order; and failures carry immediate physical-world consequences, since a courier is often already en route by the time a cancellation reaches the system. Building for scale from day one means making the right calls on this architecture before you have any real scale to test them against.

After rebuilding Sizl's dark kitchen platform and building Yapoki's delivery app from scratch, we've watched both kinds of decisions play out — the ones that survive 200-plus orders a day, and the ones that don't. The difference is usually invisible until it becomes expensive to fix. This guide walks through five architectural layers — order state, real-time infrastructure, dispatch, geo-routing, and offline handling — with the trade-offs behind each one, plus the three decisions worth making before you write your first line of code.

Every delivery app we've built had the same inflection point: the architecture that worked at 100 orders per day started showing cracks at 500. The teams that avoided rebuilding were the ones that made three specific decisions in sprint one — before they had any scale to worry about.
Max B.,  | dev.family
Max B.
Management

We'll get to those decisions, but first it's worth understanding why delivery apps break in ways that generic marketplace apps don't.

The Three Properties That Make Delivery Architecture Different

Delivery is a different class of engineering problem than e-commerce with courier tracking bolted on, for three reasons — and understanding them is what lets you design correctly on day one instead of retrofitting later.

Sub-minute state transitions. A delivery order moves through 8 to 12 statuses in 25 to 40 minutes, and every transition has a real-world consequence: a courier gets dispatched, the kitchen starts cooking, a payment gets captured. In standard e-commerce, an order can sit in "processing" for hours without anyone noticing; a delivery order can't sit anywhere for more than a few minutes before something breaks. That's why a state machine has to exist from day one instead of getting bolted on once things are already live. Implicit state, modeled through boolean flags or nullable timestamps, is where race conditions come from under concurrent load.

Three concurrent real-time consumers. The customer wants ETA updates, the courier needs dispatch and navigation data, and the kitchen needs to see the order queue change — all three are watching overlapping data at the same time, and an update to one has to reach the others immediately. A REST API handles read-heavy operations fine, but it can't carry real-time state distribution on its own — you need a dedicated real-time layer, whether that's WebSocket or Server-Sent Events.

Physical-world coupling. Delivery data is tied to things happening in the real world. A courier is moving, so location has to refresh every 5-10 seconds. A dish runs out at the kitchen, so it has to disappear from the menu immediately. An order gets cancelled, but the courier may already be on the way. Menu availability and order state need synchronous updates with real conflict resolution — eventual consistency is too slow when a courier is already standing at the counter.

These three properties play out operationally too, not just in code. And if your stack is React Native, the framework has its own opinions on how to model this state — we cover those specifics in FoodTech App Architecture in React Native.

If you're weighing whether to build this in-house or bring in a team that has already shipped two production delivery platforms, our custom delivery platform development work covers exactly this layer.

Curious how these same properties show up once you're running multiple dark kitchen brands on one stack? - dev.family

Curious how these same properties show up once you're running multiple dark kitchen brands on one stack?

Read the article

Real-Time Order Flow: State Machine Design and WebSocket Architecture

The order state machine is the central architectural artifact of a delivery app — everything else gets built around it.

With Sizl, we rebuilt an existing dark kitchen app in 2.5 months. The previous version was built in Kotlin Multiplatform — functional but hard to iterate on. When we rebuilt in React Native with a monorepo structure, the first thing we designed was how the order state machine would work under concurrent load, and what happens when a courier loses connectivity mid-delivery.
Max B.,  | dev.family
Max B.
Management

Define the state machine explicitly. A typical delivery flow looks like this: CREATED → PAYMENT_PENDING → CONFIRMED → PREPARING → READY → PICKED_UP → DELIVERING → DELIVERED, with CANCELLED as a branch that can fire from multiple entry points, each with its own business rules and compensation logic (refund vs. partial refund, courier reassignment, kitchen notification). Skip the explicit machine and you get bugs that only surface under concurrent load — two conflicting writes racing to update the same order with no single source of truth for what state it's actually in.

Real-Time Order Flow: State Machine Design and WebSocket Architecture

Pick your real-time transport deliberately. There are three real options, and they're not interchangeable:

Approach

How it works

Where it fits

Polling

Client asks every N seconds

Fine for an MVP; at 1,000 active orders it's thousands of HTTP requests per minute — not production-grade

Server-Sent Events

One-way server-to-client push

Good for order status and courier location feeds where the client only reads

WebSocket

Full-duplex

Needed wherever the client also sends data, like a courier app streaming GPS coordinates

WebSocket buys you the lowest latency but costs you connection management, reconnection logic, and message queuing for dropped connections — it's not free complexity, so reserve it for the direction of communication that actually needs it.

In Yapoki, we used sockets to push order status updates to the customer without a manual refresh, and built the live kitchen feed as a streaming view through a WebView — not every mobile media player supports direct streaming links, and the WebView sidesteps that limitation entirely.

Add an availability check at checkout, not just at browse time. This is a pattern from Sizl: after the customer confirms an order, the app fires an additional request to the backend to verify every line item is still available before the order is committed. If something dropped out of stock in the interval, a pop-up surfaces exactly what's missing. This single extra round-trip closes one of the most common operational failures in delivery — a confirmed order for a dish the kitchen can no longer make.

See how this kind of real-time architecture holds up under load more broadly in Online Orders Without Downtime: How Restaurants and Retailers Can Ensure Stability and Growth.

Watched your order states desync the last time traffic spiked?

Dispatch Logic: How to Route Orders to Couriers at Scale

Dispatch is the hardest algorithmic piece of a delivery app. The math itself is simple; what makes it hard is that it runs on real-time data that's constantly shifting, with immediate physical consequences whenever it gets something wrong.

There are three dispatch strategies, and the right one depends on volume, not ambition:

Manual dispatch. An operator assigns couriers by hand. It doesn't scale, but it's a valuable fallback when automation fails, and every production dispatch system should keep it running as a standing emergency mode even after automation ships.

Rule-based dispatch. Assignment follows fixed rules: nearest available courier, zone-based routing, simple load balancing. Straightforward to build, and it holds up fine at moderate volume. Its blind spots: it doesn't account for a courier's current speed, live traffic conditions, or multi-stop routing.

Optimization-based dispatch. This factors in courier location, speed, current load, kitchen prep ETA, delivery ETA, and zone constraints together. It's more work to build and it pays off at higher volume. In our experience, the switch from rule-based to optimization-based makes sense when rule-based dispatch starts producing visible delays during peak hours — not at some predetermined order count.

Dispatch Logic: How to Route Orders to Couriers at Scale

Build in an operational pressure valve. Sizl's High Demand Mode is worth copying directly: when load crosses a threshold, the app automatically shows a banner that limits incoming orders. It's a deliberate design choice that protects customers who already have orders in flight, at the cost of pausing new ones for a while. An alternative to a hard cutoff is simply inflating the quoted ETA instead of blocking orders outright.

Plan for multi-stop routing and dispatch failure. A single courier can carry multiple orders when delivery radii overlap, which improves efficiency but complicates ETA accuracy for every recipient on the route. You also need an answer for what happens when a courier doesn't accept an assignment within N seconds — reassignment with an escalation timeout belongs in production from day one.

This is exactly the kind of routing complexity that shows up in a multi-brand dark kitchen running several concurrent order streams — see Ghost Kitchen Software: Multi-Brand Dark Kitchen Tech for how dispatch logic adapts to that setup. If you're building for that kind of virtual-restaurant model, our virtual restaurant and dark kitchen technology work covers the operational side of this too.

Dispatch delays showing up in your peak-hour numbers?

Geo-Routing: Building Location Services That Work Under Load

Geo-services in a delivery app carry real architectural weight, well beyond dropping in a maps SDK — these are decisions that show up both in your API bill and in degraded UX when they're made carelessly.

There are five distinct geo-operations, each with its own decision to make:

Address geocoding. Converting an address to coordinates. In Yapoki, the geocoder we used has a hard limit of 200 address searches a day across all users. The fix is caching results for frequently searched addresses, rate limiting the endpoint, and keeping a cheaper fallback provider ready — a bigger plan doesn't solve a caching problem.

Nearest restaurant or kitchen detection. Figuring out which location actually serves a given address. Yapoki's geocoder automatically finds the nearest restaurant and loads its catalog the moment an address is entered. One detail that matters more than it looks: delivery zone boundaries need to be polygon-based, not circle-based — real delivery areas are never circles.

Geo-Routing: Building Location Services That Work Under Load

Courier real-time location. Refreshed every 5-10 seconds while an order is active — this is the pattern behind Sizl's real-time courier tracking on the map. Three techniques matter here: location smoothing to filter out GPS jitter, dead reckoning to predict position between updates based on the last known vector, and background location permissions, which iOS and Android handle very differently.

ETA calculation. A naive ETA is just distance divided by average speed. A production ETA is kitchen prep time plus courier assignment time plus routing time plus a traffic factor — and the gap between the two is typically 10-15 minutes during peak hours. Yapoki's approach shows the average delivery time based on the nearest restaurant's current load, not a static estimate.

Delivery zone enforcement. Sizl displays every available dark kitchen and its delivery boundary directly on the map. If a customer is outside every zone, a "Suggest Point" button lets them request a new location — which is both graceful degradation for that customer and a real signal for where to expand next.

On the mapping provider itself: Google Maps has the most complete US coverage but costs more; MapBox is cheaper with better customization but thinner coverage in some regions; HERE is strong for enterprise fleet management; OpenStreetMap is free but you host it yourself. The right choice depends on your market and your API budget more than any inherent technical superiority.

Still deciding whether to build geo-routing custom or lean on a platform's built-in tools? - dev.family

Still deciding whether to build geo-routing custom or lean on a platform's built-in tools?

Read the article

Offline Resilience: What Happens When Connectivity Breaks

A courier loses signal in a parking garage, a kitchen display freezes when the router reboots, or a customer places an order from a dead zone — in a delivery app, all three happen every single day, so the architecture has to expect them rather than treat them as edge cases.

There are three scenarios worth designing for explicitly:

Customer app, placing an order on bad connectivity. Optimistic UI confirms the order visually right away while the actual sync with the backend happens once connectivity returns. The risk: an item could become unavailable while the order is "in flight." The fix is an idempotent order-creation endpoint paired with an availability re-check the moment the sync completes.

Courier app, losing connection mid-delivery. A courier can't be left stuck with no way to update their status. Local state storage plus a queue for accumulated status updates, synced when the connection returns, solves this — and location updates specifically should be buffered and sent as a batch on reconnect rather than dropped.

Offline Resilience: What Happens When Connectivity Breaks

Kitchen display, losing connection. No connection means no new orders appear on screen, which is worse than it sounds during a rush. The fix is a local queue holding the last known state, a visible indicator that the connection is down, and automatic reconnection with exponential backoff.

The deeper architectural question underneath all three is local-first versus online-first. Online-first means every operation requires a live connection — offline is a degraded or blocked experience. Local-first means the app keeps working fully offline and syncs in the background. For a delivery app, the right answer is usually hybrid: browsing and cart management can be local-first, but checkout and order status should stay online-first, because those carry physical-world consequences that need backend confirmation before they're real.

We go deeper on implementing these patterns in React Native in How to Build Local-First Apps with React Native + RxDB: Architecture and Examples.

Not sure if your app needs local-first or just better error handling?

Monorepo Architecture: One Codebase for Customer, Courier, and Kitchen Apps

A delivery ecosystem is never one app. At minimum, it's a customer app, a courier app, and a kitchen display — and a monorepo lets you build all three in parallel with shared components instead of duplicating logic three times over.

Case study: how Sizl's monorepo cut a second app build from weeks to days

When Sizl needed a dedicated riders app after launching the customer-facing product, the team structured the codebase as a monorepo: customer app, courier app, and a support tool sharing one codebase, each with its own folder, logic, and flow, but unified underneath. As the team put it, "this setup enables us to reuse UI components, authentication logic, API integrations, and more." A concrete example of the payoff: the support app's chat feature will simply reuse the ready-made module already built for the customer app — same interface, same logic, shipped in a couple of days instead of built from scratch.

Case study: how Sizl's monorepo cut a second app build from weeks to days

The riders app itself was built and released in 2.5 weeks, compared to an estimated 6-8 weeks starting cold — almost entirely because authentication, API integrations, and base UI components already existed in the shared codebase. That same rebuild, moving Sizl's dark kitchen platform off Kotlin Multiplatform onto React Native, took 2.5 months end to end and shipped ahead of a $3.6M seed round.

Want to see the full Sizl monorepo build in detail? - dev.family

Want to see the full Sizl monorepo build in detail?

Case

The trade-offs are real in both directions. A monorepo gives you one deployment pipeline, shared types that eliminate drift between apps, and features built for one app that adapt easily to another. It costs you longer build times as the codebase grows, and it demands discipline about what's shared versus app-specific — onboarding a new developer takes longer when they have to learn where that line is. Below three developers working on a single app, a monorepo is premature optimization. Past two apps sharing real domain logic, it pays for itself with the first shared feature you build.

The anti-pattern to avoid: business logic on the client

Yapoki's build surfaced the opposite lesson. Catalog management, promo codes, and discount logic were all handled client-side: "all work with the catalog — managing order details, displaying dishes in categories, applying promo codes and other actions — was done on the client side." The consequence was concrete: "although we had access to the backend, we were unable to make the necessary changes and transfer the data correctly." Business logic living on the client blocks backend-side optimization and turns every new channel — web, kiosk, a partner integration — into a fresh reimplementation of rules that should have lived in one place.

This same "one codebase, multiple front ends" thinking scales well past a three-app delivery stack — see how a 40-brand restaurant holding structured theirs in Own Restaurant Delivery App, and the React Native specifics in FoodTech App Architecture in React Native.

Weighing a monorepo against separate repos for your own stack? - dev.family

Weighing a monorepo against separate repos for your own stack?

Read the article

Scale Checklist: Three Decisions to Make Before Sprint One

You don't need to build for 10,000 orders a day on day one. But three architectural decisions are worth making before you write your first line of code, because reversing them later is expensive.

Decision 1: An explicit order state machine, from sprint one. The state machine is the schema-level contract every part of the system agrees to. Change it after the customer app, courier app, kitchen display, and backend are all already written around implicit state, and you're rewriting all four. A minimal MVP state machine needs 6-8 statuses with explicit transitions, a CANCELLED branch with entry points from every status up to DELIVERING, and compensation actions defined for each transition.

Decision 2: A real-time layer from day one, even before you need it. Bolting WebSocket onto a REST-only architecture that every client already depends on is 3-4 weeks of refactoring. Designing with the real-time layer in from the start is 3-4 extra days up front. A reasonable MVP split: SSE for order status and courier location where the server only pushes, WebSocket for the courier app where data flows both ways, and polling reserved strictly as a fallback.

Decision 3: Business logic on the backend, never the client. The anti-pattern from Yapoki — catalog management, promo validation, and discount calculation living client-side — makes it impossible to change backend behavior without breaking every client that depends on it. The rule that avoids this: anything the business can change — prices, discounts, availability — lives on the backend, and the client stays a presentation layer.

The teams that avoid rebuilding at scale are the ones who understood which architectural decisions are expensive to reverse — and got those right in sprint one.
Max B.,  | dev.family
Max B.
Management

For a broader view of sequencing an MVP toward a scale-ready product, see MVP Development for FoodTech Startups: How to Go from Idea to Investor-Ready Product in 12 Weeks. And if you want the failure modes spelled out directly, 5 Ways to Kill a FoodTech App Before Users Even Fall in Love With It covers the architectural mistakes that kill delivery apps before they reach scale.

Key Takeaways

  • Delivery architecture differs from e-commerce in three specific ways: sub-minute state transitions, three concurrent real-time consumers, and physical-world coupling that rules out eventual consistency.
  • Build an explicit order state machine in sprint one — retrofitting it after customer, courier, kitchen, and backend all assume implicit state means rewriting all four.
  • Design your real-time layer (SSE for one-way feeds, WebSocket for bidirectional courier data) before launch, not after — retrofitting it costs weeks, designing it up front costs days.
  • Match dispatch complexity to actual order volume: rule-based works fine under 100-300 orders a day; optimization-based earns its complexity above that, and only when rule-based visibly starts lagging at peak hours.
  • Keep business logic — pricing, discounts, availability — on the backend. Client-side catalog logic is the single anti-pattern most likely to block you from shipping new channels later.
  • A monorepo pays off once you have two or more apps sharing real domain logic; below that, it's premature structure you'll be maintaining for no benefit yet.
  • Build an operational pressure valve (a demand cap, an inflated ETA) into dispatch before you need it — it's cheaper to have and not need than to build under fire during a traffic spike.
AnnaS, Business Development Manager - dev.family

Building a delivery platform? Let's talk architecture before you write your first line of code.

Anna S., Business Development Manager

FAQ

You may also like: