Protocols, real-time options and pagination

Lesson 2.4 · 8 min read · by Vinay C

We'll cover the following

REST, gRPC, GraphQL, JSON-RPC

ProtocolWhat it isBest for
REST over HTTP/JSONResources plus verbs (GET /orders/42)Public APIs, browser and mobile clients. The default.
gRPCBinary (Protobuf) over HTTP/2, strict typed contracts, streamingService-to-service calls inside your system: fast, small, typed
GraphQLThe client asks for exactly the fields it wants, from one endpointWeb and mobile apps that need flexible data from many services
JSON-RPCCall a named method with JSON ({"method": "getUser"}) over HTTPSimple 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 .proto file 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

OptionHowUse when
PollingThe client asks every few secondsSimple, low stakes. Wasteful at scale.
Long pollingThe server holds the request open until there is newsYou cannot use WebSocket or SSE but need near-real-time
WebSocketOne persistent two-way connectionChat, games, collaborative editing
SSE (server-sent events)The server pushes one way over plain HTTPLive feeds, in-app notifications, progress updates
WebhooksServer-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:

StyleHowGoodWatch 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 + 1 rows: 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 LastEvaluatedKey is a cursor, and Elasticsearch caps deep from + size paging, so use search_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."