Browse Templates/Food Delivery App

Food Delivery App

A complete food delivery database schema designed for on-demand food ordering platforms. Includes restaurant management with menu items, customer profiles, order processing with line items and status tracking, and delivery driver assignment. Suitable for building apps like Uber Eats, DoorDash, or custom food delivery solutions.

PostgreSQL6 tablesMobile
fooddeliveryrestaurantmobileorders
Created with ERD Studio

About this schema

An order points at three parties at once — customer, restaurant and driver — and the driver is nullable. That nullability is the whole dispatch model: an order exists and is being cooked before anyone has been assigned to carry it, and assignment is an update to driver_id rather than a row in a queue table.

order_items copies unit_price from the menu item instead of reading it later. Restaurant prices move constantly, and a receipt that recalculates from today's menu would quietly change what a customer paid last Tuesday. The menu_item_id stays so the dish can still be identified; the number beside it is what was actually charged.

The delivery address is written twice on purpose. customers.delivery_address is the saved default that fills the form, and orders.delivery_address is where this particular order went. Without the second one, moving house would rewrite the destination of every order already delivered.

Key relationships

restaurants → menu_items → order_items
The menu belongs to the restaurant, and an order line names the dish. is_available lets an item be hidden without deleting the rows that reference it.
orders → customers / restaurants / drivers
Three keys on one row. driver_id is nullable because an order is placed and accepted long before anyone picks it up.
orders → order_items
The lines carry quantity, the price as charged and special_instructions — the note belongs to the line, not the order, because it is about one dish.
drivers.is_available
Dispatch state on the driver rather than a separate assignment table, so finding someone to send is one indexed read.
orders.ordered_at / delivered_at
Two timestamps instead of a status history. Their difference is the delivery time every operational report is built on.

Design decisions

The driver is a column on the order, not a dispatch table

A proper dispatch system records offers, rejections and reassignments, and that is a table of its own. A single driver_id says only who has it now. For a schema this size that is the right trade — assignment is one update and the live map is one query — but it means a rejected offer leaves no trace, and the question of how many drivers turned this order down has no answer here.

Prices are copied onto order lines

menu_items.price is the current price; order_items.unit_price is the historical one. Joining to the menu for a receipt is simpler and wrong — a price change or a removed dish rewrites or breaks every past order. The duplication is the cost of an order staying true to itself after the menu moves on.

Ratings are cached on restaurants and drivers

Both carry a rating column with no reviews table behind it. Listing restaurants by rating is the most common read in the product and it needs a sortable number on the row. The limitation is plain: there is nowhere to store who rated, what they wrote, or how to recompute the average — adding reviews later means this column becomes a cache rather than the record.

Order state is one status column plus two timestamps

status answers where the order is now; ordered_at and delivered_at answer how long it took. A full state-transition log would answer when it was picked up and how long it sat at the restaurant, which this schema cannot. It is the deliberate floor — enough to run the product, with the event log as the known next addition.

Tables in this template

restaurantsRestaurants
8 cols
menu_itemsMenu Items
8 cols
customersCustomers
6 cols
ordersOrders
10 cols
order_itemsOrder Items
6 cols
driversDrivers
7 cols

Frequently asked

Why is driver_id nullable on orders?
Because an order is placed, paid and cooking before a driver is assigned. Requiring one would mean inventing a placeholder driver for every new order, and every dispatch query would then have to exclude that fake row.
Can one order contain food from two restaurants?
Not as modelled — orders has a single restaurant_id. Multi-restaurant baskets need an order that groups several per-restaurant sub-orders, since each kitchen cooks and is paid separately.
Why not join menu_items for the receipt price?
Because the menu is live data. A dish repriced this morning would retroactively change last month's receipts, and a deleted dish would leave an order line with no price at all.
Where do delivery fees and commission live?
orders.delivery_fee holds what the customer paid for delivery. Restaurant commission and driver payout are not modelled — they belong in their own settlement tables, because they are calculated on a different schedule from the order.
How do I find available drivers near an order?
The is_available flag on drivers is the first filter; proximity is not in this schema. Real dispatch adds a location column updated continuously, usually with a geospatial index, which is intentionally outside what this template models.