Skip to content

The phone app makes five calls to draw one screen

  • System Design · Foundations
  • 20 min read, about 1 h with practice
  • Concept
  • Asked often: medium
  • Reviewed 2026-11

Your shop’s phone app has an “Account” screen: the user’s name, and their last 3 orders with the item names. Your API has one address for each kind of thing: users, orders, items.

To draw that one screen, the app makes 5 requests. It must wait for the orders before it can ask for their items. On a train, with 300 ms for each round trip, the screen takes over half a second. And most of the data it downloads is never shown.

  • What you have: a phone app, a web app, partner companies, and several services of your own that call each other.
  • What you want: each screen in as few round trips as possible, with only the data it shows; fast, typed calls between services; and an API partners can use with any tool.
  • The hard part: these needs pull in different directions. No single style is best for all of them.
Hint 1 of 3

Right now the server decides what each answer contains.

Hint 2 of 3

What if the client sent a list of the fields it wants?

Hint 3 of 3

Calls between your own services happen thousands of times a second. Does text like JSON make sense there?

Think of three ways to order food. REST is a menu: each dish has a number, and you order dishes one by one. GraphQL is telling the waiter exactly what you want on one plate. gRPC is the kitchen’s own order slips: short, fixed, and fast, but only the staff use them.

  • REST uses HTTP as it is. Each resource (a user, an order) has a URL, and the method says what to do: GET /orders/100, POST /orders, DELETE /orders/100. Answers are usually JSON. GET answers can be cached by browsers and CDNs.
  • GraphQL has one endpoint, usually POST /graphql. The client sends a query that names the fields it wants, and gets back exactly that shape.
  • gRPC calls a function on another server, like GetOrders(user_id=7). Messages are Protocol Buffers, a compact binary format, defined in a shared file. It runs on HTTP/2 and can stream messages both ways.
query {
user(id: 7) {
name
orders(last: 3) { created items { name } }
}
}

Watch the screen load over REST, then GraphQL, then see where gRPC fits. Change the round trip time below the player.

One screen, three API styles

v1: REST

Parts:

  • Phone app
  • REST API
  • Database

Arrows:

  • Phone app sends requests to REST API
  • REST API sends requests to Database

Step 1 of 7. A phone screen shows a user’s name and their last 3 orders, with item names. On a phone network, one round trip takes 300 ms here.

Try your own input

The counts come from this model, which the tests run. It also writes numbers the way Protocol Buffers do, to compare their size with JSON:

Requests, rounds and bytes for one screen
"""One phone screen: a user's name and their last 3 orders, with item names.
Three ways to fetch it, counting requests, rounds of waiting, and bytes sent
back. A round is one wait for the network: requests in the same round go out
together.
"""
import json
USER = {
"id": 7,
"name": "Asha",
"email": "asha@example.com",
"phone": "+44 20 7946 0000",
"address": "1 High St, London",
"joined": "2021-04-02",
"plan": "plus",
"avatar_url": "https://img.example.com/u/7.png",
"bio": "Loves tea and trains.",
"language": "en",
"timezone": "Europe/London",
"marketing_opt_in": False,
}
ORDERS = [
{
"id": 100 + i,
"user_id": 7,
"status": "delivered",
"total": 25.5 + i,
"currency": "GBP",
"created": f"2026-10-0{i + 1}",
"address": USER["address"],
"payment": "card",
"tracking": f"TRK{i}000",
"notes": "",
}
for i in range(3)
]
ITEMS = {
o["id"]: [
{
"id": o["id"] * 10 + j,
"order_id": o["id"],
"name": f"Item {j}",
"price": 8.5,
"sku": f"SKU-{j}",
"weight_g": 400,
"image_url": f"https://img.example.com/{j}.png",
}
for j in range(2)
]
for o in ORDERS
}
def size(body: object) -> int:
return len(json.dumps(body).encode())
def rest() -> dict[str, int]:
"""GET /users/7 and GET /users/7/orders together, then GET /orders/{id}/items for each."""
bodies = [USER, ORDERS, *ITEMS.values()]
return {"requests": len(bodies), "rounds": 2, "bytes": sum(size(b) for b in bodies)}
def graphql() -> dict[str, int]:
"""One query that asks for exactly the fields the screen shows."""
body = {
"data": {
"user": {
"name": USER["name"],
"orders": [
{
"created": o["created"],
"items": [{"name": i["name"]} for i in ITEMS[o["id"]]],
}
for o in ORDERS
],
}
}
}
return {"requests": 1, "rounds": 1, "bytes": size(body)}
def varint(n: int) -> bytes:
"""How Protocol Buffers (used by gRPC) write a whole number: 7 bits a byte."""
out = bytearray()
while True:
byte = n & 0x7F
n >>= 7
if n:
out.append(byte | 0x80)
else:
out.append(byte)
return bytes(out)
def ids_as_json(ids: list[int]) -> int:
return size({"order_ids": ids})
def ids_as_protobuf(ids: list[int]) -> int:
"""A packed repeated field: one tag byte, a length, then each id as a varint."""
packed = b"".join(varint(i) for i in ids)
return 1 + len(varint(len(packed))) + len(packed)

Each rung serves the same Account screen: a name and 3 orders with 2 items each.

Okay

REST resources as they are

The idea: GET /users/7 and GET /users/7/orders together, then GET /orders/{id}/items for each order.

This is the right first API. It is simple, each URL can be cached, and anyone can call it with curl.

Cost
5 requests, 2 rounds
Weak spot
about 1,750 bytes for under 300 used
Why
The items wait for the orders, and every answer sends the whole record.

Where it breaks: the phone. Two rounds of waiting at 300 ms each, plus email, address and tracking fields the screen never shows (over-fetching). Each new screen needs either more requests or a new endpoint.

Better

GraphQL for the apps

The idea: One query names the exact fields. The GraphQL server fetches them inside the data centre and sends one answer.

The waste in Okay: the phone crossed the slow network twice, for data it threw away. Now the server does the joining, close to the data, and the answer has under 300 bytes.

Cost
1 request, 1 round
Weak spot
harder to cache; costly queries
Why
Calls between servers in one data centre take about 1 ms, so the slow phone link is crossed once.

Where it breaks: queries are all POST to one URL, so CDNs can’t cache them by address. A client can ask for orders of orders of orders, so you must limit query depth and cost. And the GraphQL server now calls other services thousands of times a second, as JSON text.

Best

The right style for each connection

The idea: GraphQL (or a REST endpoint per screen) for your apps, gRPC between your services, and plain REST for partners.

The waste in Better: the inside calls used text and no fixed contract. gRPC gives each service a typed contract in a .proto file, generated clients in many languages, and streaming. Browsers can’t make raw gRPC calls, which is why it stays inside.

Cost
1 round for apps; small, fast inside calls
Weak spot
three styles to run and learn
Why
Protocol Buffers sent 1,000 ids in 3,003 bytes, against 8,015 as JSON, and the contract is checked when code is built.

When Best isn’t best: a small team with one web app and one service should use REST everywhere. Three styles mean three sets of tools, docs and ways to fail. Add GraphQL when screens multiply, and gRPC when inside calls are many and slow.

Step 1 is a plain REST API. Step 2 puts an API gateway in front, a GraphQL server behind it, and two services it calls.

Step 1: One API for a phone app

  • A phone app shows users, orders and items.
  • Start with one app server and one database behind a REST API.
Add parts from the list above. Then press the + on a part to draw an arrow.

Solid arrow: sends requests toDashed arrow: copies data to

Watch the traffic

Send 10 requests through your board and see where each one goes. Then make a part fail and send them again.

Send
How requests move

Requests start at the Client and follow your arrows. Reads try a cache first (4 in 5 are found there), then a read replica, then the database. Writes go to the database. Background jobs go on a queue, and a worker saves them. A load balancer skips a part that is down; nothing else can.

StyleFormatStrong atWeak atUse it for
RESTJSON over HTTPSimple, cacheable, every tool knows itMany round trips, over-fetchingPublic and partner APIs, simple apps
GraphQLJSON, one endpointOne request per screen, exact fieldsCaching, costly queries, N+1 on the serverApps with many screens and clients
gRPCProtocol Buffers on HTTP/2Small, fast, typed, streamingBrowsers need a proxy; hard to read by eyeCalls between your own services
  • GraphQL’s N+1 problem. A naive server fetches the items for each order with its own query. Batch them (a “DataLoader” collects the ids and asks once).
  • No limits on GraphQL queries. One deep query can ask for millions of rows. Set a maximum depth and a cost budget.
  • Changing a gRPC message carelessly. Never reuse a field number for a new meaning; old clients will misread it. Add new fields with new numbers.
  • Returning huge lists. Any style needs pagination for big lists. The API design lesson covers it.
  • Choosing by fashion. Say the need first (“the phone makes 5 calls per screen”), then the style that fixes it.

Questions to ask first

  • Who calls this API: our own apps, partner companies, or our own services?
  • How many different screens or clients are there?
  • Do clients need live updates, or is asking now and then enough?

A 60-second walkthrough

“For partners I’d expose REST: resources as URLs, methods for actions, JSON, and status codes, so any tool works and GETs can be cached. Our phone app has many screens, and each one made several calls over a slow network. So I’d put GraphQL, or a backend-for-frontend, in front of the apps: one request per screen, only the fields it shows. Between our own services, I’d use gRPC: typed contracts, small binary messages and HTTP/2. I’d choose per connection, not one style for everything.”

Likely follow-ups

  • How do you version a REST API? Add fields without removing old ones. For breaking changes, use /v2/ in the path or a header, and keep v1 running for a while.
  • How do you cache GraphQL? Cache inside the server per object, or use “persisted queries” sent as GET with an id, so CDNs can cache them.
  • Why can’t a browser call gRPC? Browsers don’t give pages the low-level HTTP/2 control gRPC needs. gRPC-Web, with a proxy, fills the gap.
  • Live updates? Server-sent events or WebSockets for browsers; gRPC streaming between services.

Red flags interviewers notice

  • “GraphQL is always better than REST.”
  • Verbs in REST URLs, and 200 for every answer.
  • Calling a slow chain of services one after another for each screen, without saying what it costs.

Design REST endpoints for a library app: list books, see one book, add a book, change a book’s shelf, remove a book, and borrow a book.

Hint 1 of 3

Name the things (resources) as plural nouns.

Hint 2 of 3

Use the method for the action: GET, POST, PATCH, DELETE.

Hint 3 of 3

Borrowing creates something new. What is it called?

Show the answers
Okay

One address per action

The idea: GET /getBooks, POST /addBook, POST /moveBook, POST /deleteBook, POST /borrowBook.

It is really remote function calls with REST’s clothes on.

Gives
it works
Weak spot
every action is a new URL to learn
Why
It ignores what HTTP methods already say, and caches can’t tell reads from changes.
Better

Resources and methods

The idea: GET /books, GET /books/42, POST /books, PATCH /books/42, DELETE /books/42.

POST /books answers 201 Created with a Location: /books/43 header.

Gives
clear and cacheable
Weak spot
borrowing is still unclear
Why
The URL names the thing; the method says what to do to it.
Best

Make the action a resource too

The idea: A loan is a thing: POST /loans with the book and member, GET /members/9/loans, and PATCH /loans/77 to return it.

If the book is already out, answer 409 Conflict.

Gives
every action fits
Weak spot
none
Why
A loan has its own data (dates, who) and its own history, so it deserves its own URL.

Delivery companies want to read your orders and update their status. They use many languages and tools. Which style do you offer, and how do you protect it?

Hint 1 of 3

Partners won’t install your tools. What does every language already have?

Hint 2 of 3

Partners can’t change their code every week. How do you change the API safely?

Hint 3 of 3

One partner’s bug shouldn’t flood your servers.

Show the answers
Okay

Give them gRPC

The idea: Share the .proto files and let partners generate clients.

Great inside your company, a high bar for outsiders.

Gives
fast calls
Weak spot
hard for partners to start
Why
Each partner needs gRPC tools, and many firewalls and tools only speak plain HTTP and JSON.
Better

REST with JSON

The idea: GET /orders?status=ready and PATCH /orders/100 with the new status, documented with OpenAPI.

Return 422 or 400 with a clear message when a status change isn’t allowed.

Gives
any partner can start in minutes
Weak spot
no protection yet
Why
Every language has an HTTP client, and OpenAPI docs can generate clients too.
Best

REST behind a gateway, with versions and keys

The idea: An API gateway checks each partner’s key, limits their requests, and routes /v1/ and /v2/; changes only add fields.

Send events to partners with webhooks (your server calls theirs) instead of making them ask every minute.

Gives
safe for years of partners
Weak spot
a gateway to run
Why
Keys show who called; limits stop one partner’s bug; versions let old code keep working.

A trading dashboard shows prices that change several times a second. 10,000 people have it open in their browsers. Prices come from a pricing service inside your company.

Hint 1 of 3

Asking "any new prices?" every second from 10,000 browsers is a lot of empty answers.

Hint 2 of 3

Can the server send a message when a price changes, without being asked?

Hint 3 of 3

Between the pricing service and your web servers, one stream can feed everyone.

Show the answers
Okay

Ask every second (polling)

The idea: Each browser calls GET /prices every second.

Simple, and fine for a few users or slow-changing data.

Load
10,000 requests a second
Weak spot
most answers say nothing changed
Why
Every browser asks whether or not anything changed, and a change can still wait up to a second.
Better

The server pushes changes

The idea: Keep a connection open to each browser (server-sent events or a WebSocket) and send each price as it changes.

Server-sent events are plain HTTP and one-way, which fits prices. WebSockets go both ways.

Load
one message per change
Weak spot
10,000 open connections to manage
Why
Nothing is sent when nothing changes, and changes arrive at once.
Best

One gRPC stream in, pushes out

The idea: Each web server holds one gRPC stream from the pricing service, and fans each price out to its own browsers.

When a browser reconnects, send the latest prices first, so it never shows an old one.

Load
the pricing service feeds a few servers, not 10,000 browsers
Weak spot
more parts; reconnects need care
Why
gRPC streaming is built for service-to-service flows; browsers get a simple push.
  1. Multiple choiceWhich is the most REST-like way to cancel order 100?
  2. Pick the patternA phone app has 40 screens, and each one makes 3 to 6 REST calls over slow networks. What helps most?
  3. Multiple choiceWhy is gRPC a good fit between your own services?
  4. Spot the bugA GraphQL server resolves this query for 50 orders. What is wrong?
    def resolve_items(order):
        return db.query("SELECT * FROM items WHERE order_id = %s", order.id)
  • REST: nouns in URLs, methods for actions, status codes, cacheable GETs. Best for public APIs.
  • GraphQL: one endpoint, the client names the fields. Fewer round trips; watch caching, query cost and N+1.
  • gRPC: typed contracts in .proto files, binary messages, HTTP/2, streaming. Best between services.
  • Choose per connection. Say the need first, then the style.

Can you do these now?

If you’re not sure of any of these, scroll back to that part before moving on.

Status: Not started