RoadmapDay 43 / 80
System DesignMonth 3 · Week 9

Day 43: Network Layer Architecture: REST, GraphQL, and WebSockets

Choose the right network paradigm per use case by understanding the real tradeoffs — request shape, over/under-fetching, and connection lifetime.

Mark this day complete

Study

Concepts

REST vs GraphQL: the over-fetching / under-fetching tradeoff

REST exposes fixed-shape resources per endpoint (`GET /users/1`, `GET /users/1/posts`) — simple to cache (HTTP caching works out of the box, Day 44), simple to reason about, but prone to OVER-fetching (an endpoint returns fields you don't need) or UNDER-fetching (you need data from 3 endpoints, requiring 3 round trips, or a waterfall of dependent requests). GraphQL exposes one endpoint with a typed schema; the CLIENT specifies exactly which fields it needs across related resources in a single query, eliminating both over- and under-fetching at the cost of losing free HTTP caching (most GraphQL traffic is a POST to one URL) and needing more server-side query-cost governance (a malicious or accidental deep/broad query can be expensive to resolve).

The practical decision: REST fits well when resource shapes are stable and consumers are relatively uniform (a small number of client apps you control); GraphQL earns its complexity when many different clients (web, iOS, Android, third parties) need different slices of overlapping data, or when a screen naturally needs deeply nested/related data in one round trip.

WebSockets: when the server needs to push, not just respond

REST and GraphQL (over HTTP) are fundamentally request-response: the client always initiates. WebSockets establish a single persistent, full-duplex connection where EITHER side can send a message at any time — the correct choice when the server needs to proactively push updates (chat messages, live notifications, collaborative cursors, live prices) rather than the client repeatedly asking "anything new?" via polling.

The real cost of WebSockets is operational: connections are stateful and long-lived, so load balancing needs sticky sessions or a shared pub/sub backplane (Redis, etc.) across server instances, reconnection/backoff logic is the client's responsibility, and you generally still need a REST/GraphQL API alongside it for anything that is naturally request-response (initial data load, mutations) — WebSockets typically augment rather than replace the other two.

See It

Visualizations

Visualization

REST vs GraphQL vs WebSockets

 RESTGraphQL
Endpoint shapeMany endpoints, fixed response shapeOne endpoint, client-specified shape
Over/under-fetchingCommon problemSolved by design
HTTP cachingFree, works out of the boxMostly lost (POST-based)
Best forStable resources, few client typesMany client types, nested/related data needs
Connection modelRequest-responseRequest-response

Visualization

Polling vs WebSocket push for "new chat message"

Polling

Client asks every 3s: "anything new?"

wasted requests when idle, up to 3s latency

WebSocket

Server pushes the instant a message arrives

near-zero latency, no wasted idle requests

Build It

Code Examples

REST under-fetching vs one GraphQL query

js
// REST: 3 round trips (or a hand-rolled aggregation endpoint) to
// render a profile page needing user + their posts + follower count
const user = await fetch('/api/users/1').then((r) => r.json());
const posts = await fetch('/api/users/1/posts').then((r) => r.json());
const followers = await fetch('/api/users/1/followers/count').then((r) => r.json());

// GraphQL: ONE round trip, client-specified shape, nothing extra
const query = `
  query ProfilePage($id: ID!) {
    user(id: $id) {
      name
      avatarUrl
      posts(limit: 5) { id title }
      followerCount
    }
  }
`;
const { data } = await graphqlClient.request(query, { id: '1' });

A minimal WebSocket client with reconnect backoff

js
function connectChatSocket(roomId, onMessage) {
  let attempt = 0;
  let socket;

  function connect() {
    socket = new WebSocket(`wss://chat.example.com/rooms/${roomId}`);

    socket.onmessage = (event) => onMessage(JSON.parse(event.data));

    socket.onclose = () => {
      const delay = Math.min(1000 * 2 ** attempt, 30_000); // exponential backoff, capped
      attempt++;
      setTimeout(connect, delay); // client owns reconnection — the server won't retry for you
    };

    socket.onopen = () => { attempt = 0; }; // reset backoff on a successful connection
  }

  connect();
  return () => socket.close();
}

Remember

Key Takeaways

  • REST: many fixed-shape endpoints, free HTTP caching, prone to over/under-fetching for complex screens.
  • GraphQL: one typed endpoint, client-specified shape, solves fetching-shape problems but mostly loses HTTP caching.
  • WebSockets are for server-initiated push (chat, live data) — a persistent, full-duplex, stateful connection, not a REST/GraphQL replacement.
  • WebSocket infrastructure needs sticky sessions or a shared pub/sub backplane across server instances, plus client-owned reconnect logic.
  • Most real production systems combine all three: REST/GraphQL for request-response, WebSockets layered on top for live push.

Do It

Practice

  1. 1Design the API shape (REST or GraphQL, and why) for a hypothetical e-commerce product page needing product, reviews, and related items.
  2. 2Implement the reconnect-with-backoff WebSocket client above against a public echo WebSocket server and verify it reconnects after a forced disconnect.
  3. 3Write a one-paragraph justification for when you would add WebSockets to an existing REST API, using a concrete feature example.