QuestionsSystem Design

Design a notifications delivery system

Real-time TransportHardSystem Design

Design real-time notification delivery to the client. Apply RADIO; compare transports and cover reconnect, ordering, and dedup.

What it tests

Transport tradeoffs (poll vs long-poll vs SSE vs WebSocket), reconnect/backoff, dedup, and resume.

Approach & answer

Requirements: deliver server-originated notifications in near-real-time, survive reconnects, avoid duplicates, keep ordering where it matters, and scale to many clients. Architecture: a single connection manager owns the transport and fans messages out to subscribers; the server pushes events; the client tracks a cursor so it can resume. Transport tradeoffs (the core of the answer): POLLING (setInterval GET) is trivial and firewall-proof but high-latency and wasteful; LONG-POLLING holds the request open until data arrives — near-real-time over plain HTTP but connection-churny; SSE (EventSource) is a one-way server→client stream with automatic reconnection and Last-Event-ID resumption BUILT IN — ideal for notifications/feeds; WEBSOCKET is full-duplex for chat/collaboration but YOU own reconnection, heartbeats, and backpressure. Pick SSE for one-way notifications; WebSocket only when the client must also push. Resilience: reconnect with EXPONENTIAL BACKOFF + JITTER (don't stampede the server after an outage); send heartbeats/pings to detect dead connections; DEDUPE with a monotonic event id and drop ids already seen; RESUME from the last-seen id on reconnect so nothing is missed; buffer while disconnected. Ordering: a per-stream sequence number lets the client detect gaps and reorder. Also handle multi-tab (a shared worker or leader election so N tabs share one connection) and coalesce bursts. Degrade gracefully: try WebSocket/SSE, fall back to long-poll where blocked.

Use this technique when

Notification bells, live feeds, presence, chat — choosing a transport and owning reconnect/dedup/resume.

Code

// WebSocket: YOU own reconnection, heartbeat, dedup, and resume.
function connect(url, onMsg) {
  let delay = 1000, lastId = 0;
  const seen = new Set();

  function open() {
    const ws = new WebSocket(url + '?since=' + lastId);
    ws.onopen = () => { delay = 1000; };                 // reset backoff
    ws.onmessage = (e) => {
      const m = JSON.parse(e.data);
      if (seen.has(m.id)) return;                        // dedup
      seen.add(m.id); lastId = m.id;                     // resume cursor
      onMsg(m);
    };
    ws.onclose = () => {
      delay = Math.min(delay * 2, 30000);                // exponential
      setTimeout(open, delay + Math.random() * 1000);    // + jitter
    };
  }
  open();
}
// SSE (EventSource) is simpler for one-way: reconnect + Last-Event-ID are built in.

References