Design a Food Delivery App (System Design)

You’ve probably done this on a hungry evening, right?

  • Alex opens a food app, sees a bunch of restaurants nearby, and taps one that looks good.
  • Alex picks a biryani and a cold drink, hits order, and pays.
  • Then comes the fun part. A little map shows up, and Alex watches a tiny scooter icon crawl across the screen, getting closer and closer to the door.

That whole thing feels smooth and almost magical. But inside it’s a big, busy system: finding restaurants near you, taking your order, finding a driver near the restaurant, and streaming that scooter’s location to your phone in near real time. It’s a favourite system design interview question because it touches so much at once. So let’s build one together, step by step.

🎯 What We’re Building

Let’s name the thing plainly before we draw any boxes.

  • A food delivery app connects three groups of people: the customer who’s hungry, the restaurant that cooks, and the delivery driver who carries the food over.
  • The customer browses restaurants, places an order, and pays. The restaurant accepts and cooks. A driver picks it up and brings it to the door.
  • The part everyone loves is the live map, where you watch the rider move toward you on a real-time map.

So our job is to build the system that makes all of that work together, smoothly, fast, and for a lot of people at once. Think Swiggy, Zomato, DoorDash, or Uber Eats.

📋 Requirements

Before designing anything, a good engineer asks: what must this thing actually do? We split that into two buckets.

  • A functional requirement is something the system must do, a feature you can point at.
  • A non-functional requirement is about how well it does those things, like how fast or how reliable it is.

Here’s what our app must do. These are the functional ones:

  • Let the customer browse restaurants near them, with menus and prices.
  • Let the customer place an order and pay for it.
  • Match the order to an available delivery driver near the restaurant.
  • Track the order live, so the customer can watch the rider approach on a map.
  • Send updates along the way, like “restaurant accepted” or “driver is here”.

And here’s how well it should do them. These are the non-functional ones:

  • Low latency. Browsing restaurants and seeing the live map should feel instant. Nobody waits patiently when they’re hungry.
  • Reliable orders. Once Alex pays, that order must not get lost. Money is involved, so we can’t be sloppy here.
  • Real-time location. The scooter on the map should move smoothly, not jump around a minute late.
  • Scalable. It should keep working during the dinner rush, when millions of people order at the same time.

Always ask before you design

In a real interview, don’t jump straight to drawing boxes. First ask the interviewer which features matter and roughly how big it needs to be. Nailing the requirements before you design is half the score.

🧩 Break It Into Services

Here’s the thing, a system this big shouldn’t be one giant program. If everything lives in one place, a small change to the menu code could accidentally break ordering, and that’s scary. So we split the app into smaller, independent pieces, each owning one job. Each piece is called a microservice, which is just a small service that does one thing and talks to the others over the network.

Let’s name the services we need:

  • Restaurant / Catalog service. Holds the list of restaurants, their menus, prices, and whether they’re open. This is what you browse.
  • Order service. Takes an order, tracks its status, and handles payment. This is the heart of the system, because money lives here.
  • Driver-matching service. Finds an available driver near the restaurant and assigns them to the order.
  • Location / Tracking service. Takes the constant stream of location pings from drivers and pushes the right ones to the right customers.
  • Notification service. Sends out the “order accepted” and “driver arriving” messages as push notifications.

Why split it up like this? A few solid reasons:

  • Each team can build and ship their service on its own, without stepping on each other.
  • If the catalog service has a bad day, ordering and tracking can keep working.
  • You can scale each one separately. The location service gets overloaded with pings, so it can grow on its own without dragging the rest along.

Want the full story on services?

This split into small independent services is a whole topic on its own. See Microservices Architecture for the deeper why and how.

📍 Finding Nearby Restaurants and Drivers

Okay, first real problem. When Alex opens the app, we need to show restaurants near Alex, not every restaurant on the planet. And later we’ll need to find drivers near a restaurant. Both are the same kind of question: “what’s close to this point on the map?”

  • A query that asks “what’s near this location” is called a geospatial query. “Geospatial” just means it’s about positions on the earth, using latitude and longitude (the two numbers that pin a spot on the map).
  • The naive way is to look at every restaurant, measure how far each one is, and keep the close ones. That works for ten restaurants. It falls apart for a million.
  • So instead we organise places by location ahead of time, so “find what’s nearby” becomes a quick lookup instead of a giant scan. That organising-by-location is called a geospatial index.

A common trick to build that index is geohashing, which chops the map into a grid of small boxes and gives each box a short code, so finding nearby places is just “look in my box and the boxes around it.”

Alex's location (lat, lng)

Find Alex's grid box

Look up box + neighbour boxes

Restaurants in those boxes

Sort by distance, show nearby ones

So instead of measuring distance to every restaurant, we just peek into Alex’s box and the boxes touching it. Fast, and it scales no matter how many restaurants exist.

🛵 Matching a Driver

Now Alex has placed the order and the restaurant is cooking. We need a driver. This is the driver-matching service’s job, and it’s basically the same nearby-search trick, but for drivers instead of restaurants.

Here’s how the matching goes:

  • Find available drivers near the restaurant, using the same geospatial index. “Available” means online and not already carrying an order.
  • Pick one. A simple rule is the closest free driver, but you can also weigh in their rating or how busy they are.
  • Offer them the order. The driver’s app buzzes and asks, “want this delivery?”

But drivers are people, so things don’t always go smoothly. We have to handle that:

  • The driver might decline. Fine, we just offer it to the next nearby driver.
  • The driver might not answer at all. So we set a timeout, a short countdown, and if they don’t respond in time, we move on to the next driver automatically.
  • We keep going down the list until someone accepts. If nobody nearby takes it, we widen the search area a bit.

Never assign without a yes

Don’t just hand the order to the nearest driver and assume they’ll do it. Always wait for an actual accept. Otherwise an offline or unwilling driver gets stuck with an order, and Alex’s food sits cold at the restaurant.

🗺️ Live Location Tracking

This is the part Alex loves, the little scooter creeping across the map. Let’s see how that works without melting our servers.

  • The driver’s app keeps sending its current location, every few seconds, to our location service. Each little update is a location ping.
  • The customer’s app needs to see those pings show up almost instantly, so the scooter moves smoothly.
  • The trick is how we get those pings from our server to Alex’s phone in near real time.

The naive idea is to have Alex’s app ask the server “where’s the driver now?” over and over. That’s called polling, and it’s wasteful, because most of the time the answer hasn’t changed yet. There’s a much better way:

  • We use a WebSocket, which is a connection that stays open both ways, so the server can push new locations to the phone the moment they arrive, without the phone having to keep asking.
  • So the driver pings us, we update the order’s location, and we push it straight down the open connection to Alex’s app. The scooter moves.
  • For updates when the app is closed, like “your order is here”, we fall back to a push notification instead.

Driver app sends location ping

Location service

Update live location store

Push over WebSocket

Customer sees scooter move

More on real-time connections

That open two-way connection is the same backbone chat apps use. Dig into it in WebSockets and Real-Time Communication.

🧾 The Order Flow

Let’s walk through one order from start to finish. An order isn’t a single moment, it moves through a series of stages, and each stage is a status we track and tell people about.

Here’s the journey:

  • Placed. Alex pays, and the order service saves it. Payment confirmed, order locked in.
  • Restaurant accepts. The restaurant gets the order on their screen and taps accept. Now they start cooking.
  • Driver assigned. The matching service finds a driver who says yes, and ties them to this order.
  • Picked up. The driver reaches the restaurant, grabs the food, and marks it picked up. Now live tracking really matters.
  • Delivered. The driver hands it over and marks it done. Order complete.

At each step, two things happen:

  • The order service updates the status, so everyone’s looking at the same truth.
  • The notification service fires off a message, so Alex isn’t left wondering. “Your order is on the way!”

Placed (paid)

Restaurant accepts

Driver assigned

Picked up

Delivered

Who sends all those messages?

The pings and alerts are handled by a service built just for this. See Design a Notification System for how that part is built.

🏗️ High-Level Design

Let’s zoom out and put all the pieces on one canvas. When you step back, the whole system is a set of boxes talking to each other.

Client apps (customer, driver, restaurant)

API gateway

Catalog service

Order service

Driver-matching service

Location service

Restaurant DB + cache

Order DB

Queue

Notification service

Live location store

Let’s read the boxes:

  • Client apps. Three of them really: one for the customer, one for the driver, one for the restaurant. They all talk to the backend.
  • API gateway. The single front door. Every request comes here first, and it routes each one to the right service. (It also handles things like checking who you are.)
  • Services. Our microservices from earlier, each minding its own job.
  • Databases. The catalog has its own store, orders have theirs. Separate services, separate databases, so they don’t trip over each other.
  • Cache. A small fast store in front of the catalog, because restaurant listings get read constantly.
  • Queue. A waiting line for messages. The order service drops a “send notification” task here, and the notification service picks it up when ready. This lets the order service move on without waiting.
  • Live location store. A separate fast store just for the driver pings, since those come flooding in.

That’s the full picture. Each part has one job, and the gateway ties them together.

📈 Scaling It

Now picture the dinner rush, millions of people ordering at once, drivers pinging their location every few seconds. One server won’t survive that. Here’s how we grow.

  • Cache the restaurant listings. Most people in an area see the same nearby restaurants. So we keep those listings in a cache, a small fast in-memory store, and serve them without hitting the database every time. Browsing stays fast.
  • Go async with a queue. Things like sending notifications don’t need to happen right now while Alex waits. So the order service drops the task in a queue and moves on. A separate worker handles it a moment later. This is called working asynchronously, meaning we don’t block and wait. It keeps the slow stuff from dragging down the fast stuff.
  • Use a fast store for live locations. Driver pings come in a flood, and they don’t need to live forever. So we keep them in a quick in-memory store (like Redis) instead of a heavy database. Reads and writes there are lightning fast.
  • Shard by city or region. Sharding means splitting one giant database into smaller pieces so no single machine holds everything. Food delivery is naturally local, an order in Mumbai never touches a driver in Delhi. So we split the data by city or region. Each region’s traffic stays on its own slice, and adding a new city just adds a new slice.

Clients

Load balancer + API gateway

Services (scaled per region)

Cache (restaurant listings)

Sharded DBs by region

Live location store

Queue for async work

Put together, this handles huge load. Browsing flies through the cache, slow work goes async through the queue, locations live in a fast store, and sharding by region keeps each city’s load on its own machines.

🧰 Tech Choices

Part of system design is not just naming pieces, it’s saying why you picked each one. Here are the main technology decisions for this system and the reason behind each.

Decision Choice Why
Find nearby restaurants/drivers Geospatial index Finds what’s nearby without scanning everything.
Track live driver location Redis (in-memory) Locations change constantly, too hot for a SQL database.
Show the live map WebSockets Pushes the driver’s moving position to the user.
Orders Relational database Durable, consistent record of each order.
Notify restaurant / async work Message queue Keeps the order request fast and reliable.

⚠️ Common Mistakes and Misconceptions

A few things trip people up on this one. Let’s clear them out.

  • “Scan all drivers to find the nearby ones.” Please don’t. Checking distance to every driver in the country for each order is painfully slow. Use a geospatial index so “find drivers near here” is a quick lookup, not a giant scan.
  • “Have the app poll the server for the driver’s location.” Wasteful. Asking “where is it now?” over and over burns battery and server power, mostly to hear “same as before.” Use a WebSocket so the server pushes new locations only when they actually change.
  • “One big database for everything.” Risky and slow. Orders, restaurant menus, and live locations have totally different needs. Give each service its own store, keep live locations in a fast in-memory store, and shard by region.
  • “Assign the order to the nearest driver, done.” Not so fast. Drivers can be offline, busy, or just decline. You need accepts, declines, and timeouts, so an order never gets stuck on someone who won’t take it.
  • “Notifications must be sent right away, in the order flow.” They don’t have to block anything. Drop them on a queue and send them asynchronously, so a slow notification never holds up Alex’s order.

🛠️ Design Challenge

Try extending the design yourself. Think each one through first, then open the answer to see a full breakdown.

Surge pricing. When demand spikes and free drivers are scarce, prices go up. How would you measure demand vs supply in a region, and where would you apply the higher price?

ETA prediction. Show a smart “arriving in 25 minutes” estimate. What signals would you use?

Batched deliveries. Let one driver carry two nearby orders at once. How does matching change, and how do you keep both customers’ tracking accurate?

🧩 What You’ve Learned

You can now design a food delivery app from scratch and talk through it clearly. Here’s what you picked up.

  • ✅ The core job: discover restaurants, place an order, match a driver, and track the rider live.
  • ✅ Functional vs non-functional requirements, and why you gather them first.
  • ✅ Splitting the app into services: catalog, order, driver-matching, location, and notifications.
  • ✅ Finding nearby restaurants and drivers with a geospatial index instead of scanning everything.
  • ✅ Matching a driver near the restaurant, handling declines and timeouts.
  • ✅ Live tracking with location pings pushed over WebSockets, with push notifications as a fallback.
  • ✅ The order flow through status stages, with a notification at each step.
  • ✅ Scaling with caching, async queues, a fast location store, and sharding by region.

Check Your Knowledge

Test what you learned. Pick an answer for each question, then click Check.

  1. 1

    How do you find restaurants or drivers near a user without scanning everything?

    Why: A geospatial index groups places by location, so you only look in the user's grid box and the boxes around it instead of scanning everyone.

  2. 2

    Why push the driver's location over a WebSocket instead of polling?

    Why: An open two-way connection lets the server push each new position the moment it arrives, instead of the app repeatedly asking.

  3. 3

    How does driver matching handle a driver who doesn't respond?

    Why: Offering to one driver with a short timeout means a slow or absent driver never freezes the match; the system moves to the next driver.

  4. 4

    Why are live driver locations kept in a fast in-memory store rather than a normal SQL database?

    Why: Pings arrive constantly and are throwaway, so a fast in-memory store absorbs the firehose and answers 'who's nearby' instantly.

🚀 What’s Next?

This case study leans on two ideas that show up across many systems. Go deeper on them next.

  • Design a Chat Application digs into real-time messaging over WebSockets, the same backbone our live tracking uses.
  • Design a Notification System shows how all those “order accepted” and “driver arriving” alerts actually get built and sent at scale.

Once you’re comfortable with those, come back and try the design challenge again. You’ll see the whole system click into place.

Share & Connect