Building Mobile Applications for a Food Delivery Ecosystem
Three apps, three roles, one order. The interesting problems in a delivery product are not UI problems — they are state, connectivity and idempotency problems.
Starter draft, written in general terms. Add specifics from the product you worked on where you are able to.
A food delivery product looks like a consumer app. Structurally it is a small distributed system where three different people act on the same record at the same time, from three different devices, on unreliable networks.
Three roles, one entity#
| App | Who uses it | What they do |
|---|---|---|
| Customer | The person ordering | Browse, order, track, manage profile |
| Delivery partner | The rider | Receive assignments, accept or decline, update status |
| Restaurant / admin | The kitchen | Manage items and availability, accept and prepare orders |
All three are views over the same order. That is the design constraint everything else follows from.
The order lifecycle belongs on the server#
The first instinct is to let each app track status locally and sync. That breaks immediately: a request fails, one app believes the order is preparing, another still shows placed, and the customer is looking at a screen that disagrees with the kitchen.
placed → accepted → preparing → ready → picked_up → delivered
↘ rejected ↘ cancelled
Rules worth enforcing centrally:
- Transitions are validated server-side.
placed → deliveredis rejected, whoever asks. - Each transition is permitted for specific roles. A customer cannot mark an order
ready. - Clients render state; they never decide it.
Once state lives in one place, a whole category of "the apps disagree" bugs stops existing.
Assume every request will be retried#
Mobile networks fail mid-request constantly — a tunnel, a lift, a handover between towers. The client cannot tell "the request never arrived" from "the response never came back", so it retries. Without protection, the retry places a second order.
The fix is idempotency: the client generates a key per logical operation and sends it with the request; the server stores the result against that key and returns the same result for a repeat.
POST /api/v1/orders Idempotency-Key: 7f3a9c2e-14b8-4c6d-9a11-3f7b2e5d8c40
This one property removes more real-world bugs than any amount of client-side retry logic.
Keeping three apps current#
Polling every few seconds drains batteries and hammers the API. What works better:
- Push notifications for meaningful state changes — an assignment offered, an order accepted.
- Refresh on focus, so returning to the app shows current state.
- A live channel only where it earns its keep — an active delivery in progress, not the whole app.
Push is a signal, not a delivery guarantee. Treat a notification as "something changed, go and look", never as the state itself.
What is worth sharing between apps#
Three Flutter apps invite consolidation, and some of it pays off:
- Worth sharing: the API client, models, authentication handling, design tokens and shared widgets.
- Usually not worth sharing: navigation and screens. The three roles want genuinely different flows, and forcing them through one structure produces conditionals everywhere.
Location, handled honestly#
Delivery tracking needs location; continuous high-accuracy tracking destroys battery life. Sample at a rate the use case actually needs, stop when a delivery completes, and request permissions with an explanation the user can act on. The platforms are strict here, and they are right to be.
Offline is a spectrum#
Full offline support is expensive. Most of the value comes from a few cheap behaviours:
- Cache the last-known state so screens open with content rather than a spinner.
- Show explicitly when data is stale, rather than pretending it is current.
- Queue write operations and replay them — safely, because the writes are idempotent.
What I would carry forward#
- Model the shared entity's lifecycle first, before any screens.
- Put transitions and permissions on the server, once.
- Make every write idempotent from day one; retrofitting it is painful.
- Share the API layer between apps, not the navigation.
- Design for a flaky connection as the normal case, not the exception.