The phone app makes five calls to draw one screen
See the problem
Section titled “See the problem”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.
Define the task
Section titled “Define the task”- 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.
Pause and think
Section titled “Pause and think”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?
Understand the idea
Section titled “Understand the idea”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.GETanswers 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.
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.
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:
"""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 sentback. A round is one wait for the network: requests in the same round go outtogether."""
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)Climb the ladder
Section titled “Climb the ladder”Each rung serves the same Account screen: a name and 3 orders with 2 items each.
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.
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.
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.
Draw it yourself
Section titled “Draw it yourself”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.
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.
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.
Compare
Section titled “Compare”| Style | Format | Strong at | Weak at | Use it for |
|---|---|---|---|---|
| REST | JSON over HTTP | Simple, cacheable, every tool knows it | Many round trips, over-fetching | Public and partner APIs, simple apps |
| GraphQL | JSON, one endpoint | One request per screen, exact fields | Caching, costly queries, N+1 on the server | Apps with many screens and clients |
| gRPC | Protocol Buffers on HTTP/2 | Small, fast, typed, streaming | Browsers need a proxy; hard to read by eye | Calls between your own services |
Common mistakes and edge cases
Section titled “Common mistakes and edge cases”- 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.
Say it in the interview
Section titled “Say it in the interview”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
GETwith 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
200for every answer. - Calling a slow chain of services one after another for each screen, without saying what it costs.
Practice
Section titled “Practice”1. Endpoints for a library (easy)
Section titled “1. Endpoints for a library (easy)”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
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.
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.
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.
2. An API for partners (medium)
Section titled “2. An API for partners (medium)”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
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.
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.
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.
3. Live prices on 10,000 screens (hard)
Section titled “3. Live prices on 10,000 screens (hard)”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
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.
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.
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.
Check yourself
Section titled “Check yourself”Cheat sheet
Section titled “Cheat sheet”- 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
.protofiles, binary messages, HTTP/2, streaming. Best between services. - Choose per connection. Say the need first, then the style.
Go deeper
Section titled “Go deeper”- GraphQL: Introduction
- gRPC: Introduction
- Protocol Buffers: Encoding
- Before this: Client–server, HTTP and HTTPS.
- Next: Latency numbers, to see what each round trip costs.
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