Protocols, real-time options and pagination
We'll cover the following
REST, gRPC, GraphQL, JSON-RPC
| Protocol | What it is | Best for |
|---|---|---|
| REST over HTTP/JSON | Resources plus verbs (GET /orders/42) | Public APIs, browser and mobile clients. The default. |
| gRPC | Binary (Protobuf) over HTTP/2, strict typed contracts, streaming | Service-to-service calls inside your system: fast, small, typed |
| GraphQL | The client asks for exactly the fields it wants, from one endpoint | Web and mobile apps that need flexible data from many services |
| JSON-RPC | Call a named method with JSON ({"method": "getUser"}) over HTTP | Simple RPC; what Ethereum nodes and many tools speak. Rare in interviews. |
gRPC vs REST
- Protobuf is binary, so messages are smaller and faster to parse than text JSON.
- HTTP/2 multiplexes many calls over one connection and supports streaming in both directions.
- The
.protofile generates client and server code, so contracts are strict. - The catch: browsers cannot call gRPC directly, and it is harder to debug by eye than JSON.
Real-time options
| Option | How | Use when |
|---|---|---|
| Polling | The client asks every few seconds | Simple, low stakes. Wasteful at scale. |
| Long polling | The server holds the request open until there is news | You cannot use WebSocket or SSE but need near-real-time |
| WebSocket | One persistent two-way connection | Chat, games, collaborative editing |
| SSE (server-sent events) | The server pushes one way over plain HTTP | Live feeds, in-app notifications, progress updates |
| Webhooks | Server-to-server callback: "call my URL when the payment completes" | Integrations between systems, not for end users |
When the app is closed none of these work, since there is no connection to push over. That is what APNs (iOS) and FCM (Android) push notifications are for.
Check yourselfNotification system: which protocol for (1) the gateway calling the notification service, and (2) live in-app alerts to the phone?
(1) gRPC between internal services (REST is fine too if simplicity matters), typed and fast. (2) SSE or WebSocket for live in-app alerts while the app is open (SSE if it is one-way, which alerts are); APNs/FCM push when the app is closed.
Pagination: how a list API returns a big result
Never return an unbounded list. Return one page plus a way to ask for the next one. There are two styles:
| Style | How | Good | Watch out |
|---|---|---|---|
| Offset (page number) | GET /orders?limit=20&offset=40 runs ORDER BY id LIMIT 20 OFFSET 40. | Simple, and the client can jump to page N. Fine for small admin tables. | Slow on deep pages: the database still reads and throws away the first 40 rows, so cost grows with the offset. Unstable: if rows are inserted or deleted between requests, items repeat or get skipped. |
| Cursor (keyset / seek) | GET /orders?limit=20&after=<cursor>. The cursor encodes the last row seen, e.g. (created_at, id). Query: WHERE (created_at, id) < (:t, :id) ORDER BY created_at DESC, id DESC LIMIT 20. | Same cost on page 1 and page 10,000 (an index seek). Stable while data changes. The default for feeds and infinite scroll. | No "jump to page 50", and no cheap total count. The sort needs a unique tiebreaker (id) and an index on the sort columns. |
GET /orders?limit=20
-> { "items": [ ...20 orders... ], "next_cursor": "eyJ0IjoiMjAyNi0xMC0yOCIsImlkIjo5MDEyfQ" }
GET /orders?limit=20&after=eyJ0IjoiMjAyNi0xMC0yOCIsImlkIjo5MDEyfQ
-> { "items": [ ...next 20... ], "next_cursor": null } // null = no more pages- Make the cursor opaque (base64 of the sort key). Clients just echo it back, so you can change the internals later.
- Fetch
limit + 1rows: if you get the extra one, there is a next page, and you do not return it. - Skip the exact total count on big tables (
COUNT(*)is expensive). Return "20+" or an approximate number. - NoSQL has the same idea built in: DynamoDB's
LastEvaluatedKeyis a cursor, and Elasticsearch caps deepfrom + sizepaging, so usesearch_after. - Sorting by a column with no index defeats it, whatever the style.
Check yourselfAn infinite-scroll feed uses offset pagination. Users report duplicate posts, and the database slows down on deep scrolls. Why, and what do you change?
New posts push every row down, so a page boundary moves and items repeat; and OFFSET N reads and discards N rows, so deep pages cost more. Switch to cursor pagination on (created_at, id) with an index on those columns, so each page is an index seek from the last row seen.
Reaching a sleeping phone: APNs and FCM
- The OS keeps one shared connection per phone to Apple (APNs) or Google (FCM). Your app cannot hold its own connection in the background, so push is the only way to reach a sleeping phone.
- At install the app gets a device token and registers it with your server. To wake the phone, your server calls APNs or FCM with that token and a small payload.
- With end-to-end encryption the server cannot read the message, and Apple and Google can see the payload. So the push is a content-free wake-up (at most a conversation id). Payloads are also capped at around 4 KB.
- On wake, the app connects, pulls new messages with its cursor, decrypts locally and builds the notification text itself. Do this on wake, not when the user taps.
- Push is best-effort: it can be dropped or delayed. Correctness comes from the cursor sync on every reconnect and app open, never from the push.
- A user with 3 devices has 3 tokens, and each device syncs on its own cursor.
Check yourselfCan the push payload say "Alice: hey" in an end-to-end encrypted chat?
No. The server cannot read the text, and Apple and Google would see it. Send a content-free wake-up, then let the device decrypt locally and build the notification text.
Summary: say it in the interview
- "REST at the edge for clients and the public API, gRPC between internal services for speed and typed contracts, WebSockets for the live chat connection."
- "Cursor pagination on an indexed, unique sort key: stable while data changes and constant cost per page. Offset only for small, rarely-changing admin lists."
- "Push is only a hint to wake the app. The guarantee is the cursor sync on reconnect."