How to Build a Food Delivery App in 2026: Architecture, Features, and Real Costs
A food delivery app looks like one product. It's actually three:
- Customer app - browse menus, order, track delivery
- Courier app - receive orders, navigate, confirm delivery
- Restaurant dashboard - receive orders, update status, manage menu
Plus a backend that coordinates all three in real time, handles payments, calculates routes, and manages state across concurrent orders.
This guide covers the full architecture, what to build in what order, and what it actually costs to ship.
The core data flow
Before touching architecture decisions, understand the order lifecycle:
Customer places order
↓
Payment processed
↓
Order sent to restaurant (push notification + dashboard alert)
↓
Restaurant confirms order (sets preparation time)
↓
System dispatches nearest available courier (or restaurant calls courier)
↓
Courier accepts order
↓
Courier arrives at restaurant → picks up order
↓
Courier en route → customer sees live location
↓
Delivery confirmed (photo + PIN or signature)
↓
Order closed → payouts calculated
Every step in this flow requires real-time state sync across at least two parties. This is the core architectural challenge.
Tech stack
Mobile apps
React Native for most teams in 2026. One codebase for iOS and Android, mature ecosystem, good performance for delivery apps. The main alternative is Flutter - comparable performance, slightly less mature third-party ecosystem for maps and payments.
Native (Swift/Kotlin) is justified only if you need custom hardware integration (specialized scanners, vehicle APIs) or performance-critical features. For a standard delivery app, it's not worth the 2x development cost.
Backend
Node.js with Express or Fastify for the main API. The event-driven model handles well the I/O-heavy workload: many concurrent connections, mostly waiting on database queries and external API calls.
WebSockets (via Socket.io or raw WS) for real-time order status and courier location. REST is not sufficient - you need push from server to client, not polling.
PostgreSQL for orders, users, restaurants, and transactional data. Relational model with ACID guarantees matters for payment and order data.
Redis for:
- Courier location (write-heavy, ephemeral, geospatial queries with
GEORADIUS) - WebSocket session management
- Order status pub/sub
- Rate limiting and caching
Maps and routing
Google Maps Platform - most complete, highest quality, most expensive. For an app where routing affects user experience directly, this is usually worth it.
2GIS - better for CIS/Central Asia markets (Almaty, Bishkek, Tashkent) where Google Maps has gaps in address data. Has a good SDK for React Native.
Mapbox - flexible, good customization, reasonable pricing.
For route calculation: Google Directions API or OSRM (open-source, self-hosted, free but requires infrastructure).
Payments
- Stripe - international, excellent developer experience, handles subscriptions and payouts
- Kaspi Pay - required for Kazakhstan market
- YooKassa - Russia and CIS
- Mbank / ElCart - Kyrgyzstan
Build payment abstraction from day one so you're not coupled to one provider.
Feature priority: what to build first
MVP (launch-ready)
Customer app:
- Browse restaurants by category and distance
- View menu with photos and descriptions
- Add to cart, checkout
- Card payment (one provider)
- Order tracking with status updates (text, not map)
- Order history
- Push notifications for order status changes
Courier app:
- Accept/decline incoming orders
- See pickup address and delivery address
- Navigation link (opens Google Maps or 2GIS)
- Confirm pickup and delivery
- Earnings summary
Restaurant dashboard (web, not mobile):
- Receive new orders with audio alert
- Accept/reject orders, set preparation time
- Mark as ready for pickup
- Basic menu management (enable/disable items)
- Daily order report
Backend:
- Order management with state machine
- Payment processing + webhooks
- Push notifications (FCM for Android, APNs for iOS)
- Basic courier dispatch (nearest available)
- Admin panel for managing restaurants and users
Phase 2 (after first 1,000 orders)
- Live courier tracking on map (customer sees courier moving)
- Courier location sharing (GPS polling every 5 seconds → Redis → WebSocket → customer)
- Ratings and reviews
- Promo codes and discounts
- Multiple payment methods
- Scheduled orders
- Restaurant analytics dashboard
- In-app customer support chat
Phase 3 (scale features)
- Surge pricing / dynamic delivery fees
- Multi-restaurant orders (order from multiple places)
- Subscription plans (free delivery pass)
- ML-based ETA predictions
- Automated courier dispatch optimization
- Loyalty program
- Corporate accounts
The courier location tracking system
This is the most technically interesting piece and where most teams make mistakes.
Naive approach (don't do this):
Courier app → POST /location every 5s → database write → customer polls GET /order/:id/courier-location every 5s
This is 12 database writes and 12 database reads per minute per active order. At 100 concurrent orders: 2,400 DB operations per minute for location alone. This will melt a small database.
Correct approach:
Courier app → WebSocket message every 5s → Redis GEOADD
↓
Customer WebSocket ← Redis pub/sub ← Node.js reads from Redis
Redis GEOADD handles millions of location updates per second. Redis pub/sub delivers to subscribed WebSocket connections with sub-millisecond latency. The database is only written when the order status changes - not for every location update.
// Courier sends location update via WebSocket
socket.on('location_update', async ({ orderId, lat, lng }) => {
// Store in Redis geospatial index
await redis.geoadd('courier_locations', lng, lat, `courier:${courierId}`);
// Publish to order channel for customer
await redis.publish(`order:${orderId}:location`, JSON.stringify({ lat, lng, timestamp: Date.now() }));
});
// Customer subscribes to order location updates
const subscriber = redis.duplicate();
await subscriber.subscribe(`order:${orderId}:location`);
subscriber.on('message', (channel, message) => {
const location = JSON.parse(message);
// Forward to customer WebSocket
customerSocket.emit('courier_location', location);
});
Order state machine
Model orders as a state machine. Every invalid transition should throw an error. This prevents race conditions and bugs like an order being both "delivered" and "cancelled."
const ORDER_STATES = {
PENDING_PAYMENT: 'pending_payment',
PAID: 'paid',
CONFIRMED_BY_RESTAURANT: 'confirmed',
COURIER_ASSIGNED: 'courier_assigned',
COURIER_AT_RESTAURANT: 'courier_at_restaurant',
IN_TRANSIT: 'in_transit',
DELIVERED: 'delivered',
CANCELLED: 'cancelled',
REFUNDED: 'refunded',
};
const VALID_TRANSITIONS = {
[ORDER_STATES.PENDING_PAYMENT]: [ORDER_STATES.PAID, ORDER_STATES.CANCELLED],
[ORDER_STATES.PAID]: [ORDER_STATES.CONFIRMED_BY_RESTAURANT, ORDER_STATES.CANCELLED],
[ORDER_STATES.CONFIRMED_BY_RESTAURANT]: [ORDER_STATES.COURIER_ASSIGNED, ORDER_STATES.CANCELLED],
[ORDER_STATES.COURIER_ASSIGNED]: [ORDER_STATES.COURIER_AT_RESTAURANT, ORDER_STATES.CANCELLED],
[ORDER_STATES.COURIER_AT_RESTAURANT]: [ORDER_STATES.IN_TRANSIT],
[ORDER_STATES.IN_TRANSIT]: [ORDER_STATES.DELIVERED],
[ORDER_STATES.CANCELLED]: [ORDER_STATES.REFUNDED],
};
function transitionOrder(order, newState) {
const validNext = VALID_TRANSITIONS[order.status] || [];
if (!validNext.includes(newState)) {
throw new Error(`Invalid transition: ${order.status} → ${newState}`);
}
order.status = newState;
order.statusHistory.push({ state: newState, timestamp: new Date() });
return order;
}
Push notifications architecture
Delivery apps live and die on notification reliability. A courier missing an order assignment because a push didn't arrive is lost revenue.
Use a dedicated service, not raw FCM/APNs calls. OneSignal, Firebase Notifications, or a custom wrapper with retry logic. Raw FCM/APNs calls have no retry, no delivery tracking, no fallback.
Critical notifications (new order for courier, order assigned for restaurant) should have a fallback:
- Push notification (FCM/APNs)
- If not acknowledged in 30 seconds → repeat push
- If not acknowledged in 60 seconds → in-app alert next time the app is opened
- For restaurants: audio alert loop until acknowledged
Don't rely on push for anything the user must act on immediately. Sound alerts and persistent banners are necessary for restaurants and couriers, who may have the app backgrounded.
What it costs to build
These are realistic estimates for a competent team building an MVP in 2026:
| Component | Hours | Notes |
|---|---|---|
| Customer mobile app (iOS + Android) | 400-500h | React Native, core screens |
| Courier mobile app (iOS + Android) | 200-280h | Simpler UX, fewer screens |
| Restaurant web dashboard | 150-200h | React, not mobile |
| Backend API + WebSockets | 350-450h | Node.js, PostgreSQL, Redis |
| Admin panel | 80-120h | Order management, restaurants |
| DevOps, deployment, CI/CD | 60-80h | Docker, VPS or cloud |
| QA and testing | 100-150h | |
| Total MVP | 1,340-1,780h |
At $25-35/hour (Eastern Europe or Central Asia development team): $33,500-$62,300 for the MVP.
At $60-80/hour (Western Europe): $80,400-$142,400.
Timeline: 4-6 months with a team of 3-4 developers.
Infrastructure costs (monthly, after launch)
| Service | Monthly cost |
|---|---|
| VPS / cloud (app servers) | $80-200 |
| Database (managed PostgreSQL) | $50-150 |
| Redis (managed) | $30-80 |
| Push notifications (OneSignal free tier) | $0-50 |
| Maps API (Google or 2GIS) | $50-500 depending on volume |
| SMS verification (Twilio/local) | $20-100 |
| CDN + storage (images) | $20-60 |
| Total | $250-1,140/month |
Infrastructure costs scale with order volume. At 1,000 orders/day you'll need more database capacity and map API calls will become the dominant cost.
Common mistakes
Building native iOS and Android separately - doubles development time and budget, usually unnecessary for an MVP.
Skipping the restaurant dashboard - restaurants won't trust an app-only workflow. A web dashboard they can run on a tablet behind the counter is necessary.
Real-time via polling - polling location or order status will kill your database. WebSockets + Redis pub/sub from day one.
Single payment provider - users in CIS markets often don't have international cards. You need at least one local payment option.
No order cancellation flow - cancellations happen constantly. You need refund logic, courier compensation rules, and restaurant notification. Not an afterthought.
Ignoring battery optimization - courier apps running GPS in the background will be killed by Android's battery optimization unless you implement a foreground service properly. This is a common bug that makes couriers "disappear" from the map.
Minimum launch requirements
Before first real order:
- All three interfaces functional (customer, courier, restaurant)
- Payment processing with real card
- Push notifications working on real devices (not simulator)
- Order state machine with all transitions tested
- Manual fallback for courier dispatch (you can assign manually from admin)
- Basic admin panel to see orders and intervene
- Error alerting (Sentry or similar) so you know when things break
- Privacy policy and terms of service (required for App Store)
You don't need live tracking, surge pricing, or ML dispatch to launch. You need to be able to take an order, get food to someone, and handle it when something goes wrong. Everything else is iteration.
Aunimeda develops mobile applications for iOS and Android - from MVP to production-ready apps with full backend integration.
Contact us to discuss your mobile project. See also: Mobile App Development, Mobile Game Development