1. Introduction
Modern web applications are no longer static request-response documents. Products like Figma, Google Docs, Slack, Miro, Notion, and Linear have transformed user expectations: modern web experiences must be collaborative, instantaneous, and reactive by default.
When two users work simultaneously on a shared canvas or document, changes must synchronize across distributed devices in under 100 milliseconds. If the network hiccups, users should be able to keep typing without interruption, and when connectivity returns, changes should automatically merge without blowing away each other's work.
User A (San Francisco) User B (Tokyo)
┌───────────────────────┐ ┌───────────────────────┐
│ Local Optimistic UI │ │ Local Optimistic UI │
│ Typing: "Hello" │ │ Typing: " World" │
└──────────┬────────────┘ └──────────┬────────────┘
│ WebSocket Frame (<50ms) │
▼ ▼
┌──────────────────────────────────────────────────────────────┐
│ Real-Time Gateway & Conflict Resolver │
│ (Pub/Sub Broker + CRDT / OT Convergence Engine) │
└──────────────────────────────────────────────────────────────┘
Designing these systems requires answering complex questions in frontend system design interviews:
- Which transport protocol fits your use case: WebSockets, Server-Sent Events (SSE), or WebTransport?
- How do you guarantee zero data loss when mobile connections drop in tunnels or switch cell towers?
- How do you resolve merge conflicts when two remote users edit the same sentence at the exact same millisecond?
- How do you process 60 updates per second of cursor positions without locking the browser's main thread and dropping frames?
This blueprint provides the definitive mental models, transport mechanics, conflict-resolution algorithms, and production TypeScript implementations needed to excel in Senior and Staff frontend interviews.
2. Why This Matters
Real-time collaboration is one of the highest-leverage discriminators in Senior (L5) and Staff (L6+) frontend engineering interviews. Interviewers use it to probe:
- Low-Level Network Mastery: Moving beyond high-level
fetch()calls to understand full-duplex TCP framing, HTTP/2 multiplexing, backpressure, and firewall behaviors. - Resilience & State Synchronization: Handling real-world edge cases like network flapping, zombie connections, out-of-order packet arrival, and clock drift across client devices.
- High-Frequency Main Thread Performance: Keeping UI frame rates at 60 FPS while consuming rapid streaming telemetry, utilizing Web Workers, micro-batching, and
requestAnimationFrame. - Data Consistency Paradigms: Understanding the profound trade-offs between centralized Operational Transformation (OT) and decentralized Conflict-Free Replicated Data Types (CRDTs).
Failure to architect real-time systems correctly leads to severe production issues: battery drain from tight reconnect loops, server crashes from thundering-herd reconnect storms, and catastrophic data corruption from naive state overrides.
3. Prerequisites
Before tackling real-time collaboration architecture, candidates should be comfortable with:
- TCP vs. UDP & HTTP Evolution: Understanding HTTP/1.1 connection limits, HTTP/2 header compression and multiplexing, and HTTP/3 QUIC streams.
- The Browser Event Loop: Microtasks vs. macrotasks, and how heavy deserialization in the main thread blocks UI interactions and degrades INP (Interaction to Next Paint).
- Immutable State & Reactivity: How state managers (Zustand, Redux, Jotai) propagate mutations and trigger selective component re-renders.
- Distributed Systems Basics: Vector clocks, idempotency keys, monotonically increasing counters, and eventual consistency.
4. Mental Model
To design real-time collaborative applications, decouple your architecture into three independent, specialized subsystems:
┌───────────────────────────────────────────────────────────────────────────┐
│ 1. TRANSPORT LAYER │
│ Manages network lifecycles, TLS handshakes, socket heartbeats, │
│ reconnection backoff, and offline outbox queuing. │
└─────────────────────────────────────┬─────────────────────────────────────┘
│ Inbound / Outbound Frames
▼
┌───────────────────────────────────────────────────────────────────────────┐
│ 2. SYNCHRONIZATION & CONFLICT ENGINE │
│ Translates raw payloads into deterministic mutations. Resolves │
│ concurrent operations via CRDTs or server-arbitrated OT. │
└─────────────────────────────────────┬─────────────────────────────────────┘
│ Normalized State Deltas
▼
┌───────────────────────────────────────────────────────────────────────────┐
│ 3. LOCAL OPTIMISTIC PRESENTATION STORE │
│ Immediately renders user intent to screen (0ms perceived latency). │
│ Rolls back cleanly on rejection; batches remote updates at 60 FPS. │
└───────────────────────────────────────────────────────────────────────────┘
The "Local-First" Principle
In traditional web apps, the server is the primary source of truth, and the client is a dumb cache that waits for roundtrips.
In collaborative real-time applications, this paradigm reverses into Local-First:
- Instant Local Execution: When a user types a letter or moves a canvas rectangle, the local state mutates synchronously and renders immediately.
- Asynchronous Consensus: An operation delta is placed in a network queue to broadcast to collaborators.
- Deterministic Convergence: Regardless of network delays, arrival order, or temporary disconnections, all clients must eventually reach the exact same state.
5. How It Works
Choosing the right protocol is the first critical decision in a real-time system design interview. Here is an exhaustive architectural comparison:
| Dimension | HTTP Polling | Long Polling | Server-Sent Events (SSE) | WebSockets (wss://) | WebTransport (QUIC) |
|---|---|---|---|---|---|
| Directionality | Unidirectional (Client pulls) | Unidirectional (Client pulls) | Unidirectional (Server pushes) | Full Duplex (Bidirectional) | Full Duplex (Bidirectional) |
| Protocol Foundation | HTTP/1.1 or HTTP/2 | HTTP/1.1 or HTTP/2 | HTTP/1.1 or HTTP/2 | TCP Upgrade (101 Switching Protocols) | HTTP/3 over QUIC (UDP) |
| Header Overhead | ~500–1000B per poll | ~500–1000B per hold | ~0B after initial headers | 2–10 bytes framing per packet | Minimal datagram / stream headers |
| Latency Profile | High (Poll interval lag) | Medium (Connection reset latency) | Low (under 20ms server push) | Ultra Low (under 5ms framing) | Lowest (0-RTT, no TCP head-of-line blocking) |
| Firewall & Proxy | 100% Friendly | 100% Friendly | 100% Friendly (standard HTTP) | Usually friendly, occasionally blocked by enterprise proxies | Modern; requires UDP support |
| Auto-Reconnection | Application-level | Application-level | Native in browser (EventSource) | Must be custom engineered | Must be custom engineered |
| Best Used For | Infrequent status updates | Legacy chat fallbacks | Live price feeds, AI token streaming, notifications | Multiplayer canvas, collaborative text, real-time gaming | High-throughput telemetry, real-time media, audio/video streams |
Protocol Selection Heuristic for Interviews
Do you need bidirectional communication?
│
┌─────────────────┴─────────────────┐
▼ NO ▼ YES
Do you need low-latency streaming? Is UDP / HTTP/3 accessible?
│ │
┌─────┴─────┐ ┌─────┴─────┐
▼ NO ▼ YES ▼ NO ▼ YES
HTTP Polling Server-Sent Events (SSE) WebSockets WebTransport
(Simple alerts) (Live feeds, LLM tokens) (Industry (Cutting-edge,
Standard) gaming / media)
6. Visual Diagram
Architecture Topology: End-to-End Collaborative Real-Time System
The diagram below illustrates how client applications, load balancers, WebSocket gateways, and pub/sub brokers interact in a production architecture:
Conflict Resolution: Operational Transformation (OT) vs. CRDTs
When two collaborators execute simultaneous mutations, how does the system resolve conflicting edits?
7. Simple Example
Building a Bulletproof, Production-Grade WebSocket Manager
A naive new WebSocket(url) fails in production within hours. Real mobile networks drop connections without firing onclose (zombie sockets), Wi-Fi drops cause reconnection storms, and unbuffered messages are permanently lost while offline.
Below is an enterprise-grade, fully typed TypeScript WebSocket Client incorporating:
- Heartbeat / Watchdog timer to kill silent zombie sockets.
- Exponential backoff with randomized jitter to prevent thundering herd crashes.
- In-memory offline outbox that replays queued messages when the connection re-establishes.
- Subscription multiplexing for multiple channels over a single socket connection.
type MessageHandler<T = unknown> = (data: T) => void;
interface SocketOptions {
url: string;
heartbeatIntervalMs?: number;
heartbeatTimeoutMs?: number;
initialReconnectDelayMs?: number;
maxReconnectDelayMs?: number;
}
export class ResilientWebSocket {
private ws: WebSocket | null = null;
private isExplicitlyClosed = false;
private reconnectAttempts = 0;
private reconnectTimer: NodeJS.Timeout | null = null;
// Heartbeat watchdog
private heartbeatInterval: NodeJS.Timeout | null = null;
private heartbeatTimeout: NodeJS.Timeout | null = null;
// Offline queue and subscription multiplexing
private messageQueue: string[] = [];
private subscribers = new Map<string, Set<MessageHandler>>();
constructor(private options: SocketOptions) {
this.options.heartbeatIntervalMs ??= 15000;
this.options.heartbeatTimeoutMs ??= 5000;
this.options.initialReconnectDelayMs ??= 1000;
this.options.maxReconnectDelayMs ??= 30000;
}
public connect(): void {
this.isExplicitlyClosed = false;
this.cleanup();
try {
this.ws = new WebSocket(this.options.url);
this.setupEventHandlers();
} catch (err) {
console.error("[WebSocket] Handshake failed, scheduling retry:", err);
this.scheduleReconnect();
}
}
private setupEventHandlers(): void {
if (!this.ws) return;
this.ws.onopen = () => {
console.log("[WebSocket] Connection established.");
this.reconnectAttempts = 0;
this.startHeartbeat();
this.flushOfflineQueue();
};
this.ws.onmessage = (event: MessageEvent<string>) => {
this.resetHeartbeatWatchdog();
// Handle Ping/Pong frames
if (event.data === "PONG") return;
try {
const { channel, payload } = JSON.parse(event.data);
const channelSubs = this.subscribers.get(channel);
channelSubs?.forEach((handler) => handler(payload));
} catch (err) {
console.warn("[WebSocket] Failed to parse message frame:", event.data);
}
};
this.ws.onerror = (error) => {
console.error("[WebSocket] Socket error observed:", error);
};
this.ws.onclose = (event) => {
console.warn(`[WebSocket] Closed (Code: ${event.code}, Clean: ${event.wasClean})`);
this.cleanup();
if (!this.isExplicitlyClosed) {
this.scheduleReconnect();
}
};
}
// 1. HEARTBEAT WATCHDOG: Prevents half-open "zombie" connections
private startHeartbeat(): void {
this.heartbeatInterval = setInterval(() => {
if (this.ws?.readyState === WebSocket.OPEN) {
this.ws.send("PING");
// Expect response within timeout window, or terminate connection
this.heartbeatTimeout = setTimeout(() => {
console.warn("[WebSocket] Heartbeat timeout. Killing zombie socket...");
this.ws?.close(); // Forces onclose and triggers reconnect
}, this.options.heartbeatTimeoutMs);
}
}, this.options.heartbeatIntervalMs);
}
private resetHeartbeatWatchdog(): void {
if (this.heartbeatTimeout) {
clearTimeout(this.heartbeatTimeout);
this.heartbeatTimeout = null;
}
}
// 2. EXPONENTIAL BACKOFF WITH JITTER: Prevents server stampedes
private scheduleReconnect(): void {
if (this.reconnectTimer || this.isExplicitlyClosed) return;
this.reconnectAttempts++;
// Exponential formula: delay = min(maxDelay, base * 2 ^ attempts)
const baseDelay = Math.min(
this.options.maxReconnectDelayMs!,
this.options.initialReconnectDelayMs! * Math.pow(2, this.reconnectAttempts - 1)
);
// Add 20% random jitter
const jitter = baseDelay * 0.2 * Math.random();
const finalDelay = Math.round(baseDelay + jitter);
console.log(`[WebSocket] Reconnecting in ${finalDelay}ms (Attempt #${this.reconnectAttempts})`);
this.reconnectTimer = setTimeout(() => {
this.reconnectTimer = null;
this.connect();
}, finalDelay);
}
// 3. OFFLINE QUEUE: Preserves outgoing mutations
public send(channel: string, payload: unknown): void {
const rawMessage = JSON.stringify({ channel, payload, timestamp: Date.now() });
if (this.ws?.readyState === WebSocket.OPEN) {
this.ws.send(rawMessage);
} else {
console.log(`[WebSocket] Offline. Enqueuing message for channel: ${channel}`);
this.messageQueue.push(rawMessage);
}
}
private flushOfflineQueue(): void {
while (this.messageQueue.length > 0 && this.ws?.readyState === WebSocket.OPEN) {
const msg = this.messageQueue.shift();
if (msg) this.ws.send(msg);
}
}
// 4. MULTIPLEXED SUBSCRIPTIONS
public subscribe<T>(channel: string, handler: MessageHandler<T>): () => void {
if (!this.subscribers.has(channel)) {
this.subscribers.set(channel, new Set());
}
this.subscribers.get(channel)!.add(handler as MessageHandler);
return () => {
const subs = this.subscribers.get(channel);
if (subs) {
subs.delete(handler as MessageHandler);
if (subs.size === 0) this.subscribers.delete(channel);
}
};
}
public disconnect(): void {
this.isExplicitlyClosed = true;
this.cleanup();
this.ws?.close();
this.ws = null;
}
private cleanup(): void {
if (this.heartbeatInterval) clearInterval(this.heartbeatInterval);
if (this.heartbeatTimeout) clearTimeout(this.heartbeatTimeout);
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
this.heartbeatInterval = null;
this.heartbeatTimeout = null;
this.reconnectTimer = null;
}
}8. Real World Example
Multiplayer Collaborative Canvas: Presence, Cursor Throttling & Optimistic UI
Consider a Figma-like collaborative canvas. Two performance hazards destroy canvas framerates:
- Mousemove Flood: Emitting raw
mousemoveevents (up to 120Hz on high-refresh monitors) saturates the socket and drops FPS to single digits. - React Re-render Thrashing: Putting incoming cursor coordinates into standard React component state causes the entire canvas component tree to re-render 60 times per second per active user.
Here is the high-performance production solution:
import React, { useEffect, useRef, useState, useCallback } from "react";
import { ResilientWebSocket } from "@/lib/ResilientWebSocket";
interface RemoteCursor {
id: string;
name: string;
color: string;
x: number;
y: number;
}
interface CanvasDocument {
id: string;
content: string;
version: number;
}
export const CollaborativeCanvas: React.FC<{ docId: string; user: { id: string; name: string; color: string } }> = ({
docId,
user,
}) => {
const [doc, setDoc] = useState<CanvasDocument>({ id: docId, content: "", version: 1 });
const [isSyncing, setIsSyncing] = useState(false);
// Fast DOM-bypass ref for cursors to avoid React component tree re-renders
const cursorsRef = useRef<Map<string, RemoteCursor>>(new Map());
const canvasOverlayRef = useRef<HTMLDivElement>(null);
// Throttling references
const pendingCursorPosition = useRef<{ x: number; y: number } | null>(null);
const rafId = useRef<number | null>(null);
const socketRef = useRef<ResilientWebSocket | null>(null);
// 1. FAST CURSOR EMISSION: rAF-batched micro-throttling
const broadcastCursor = useCallback((x: number, y: number) => {
pendingCursorPosition.current = { x, y };
if (!rafId.current) {
rafId.current = requestAnimationFrame(() => {
if (pendingCursorPosition.current && socketRef.current) {
socketRef.current.send(`canvas:${docId}:presence`, {
userId: user.id,
name: user.name,
color: user.color,
x: pendingCursorPosition.current.x,
y: pendingCursorPosition.current.y,
});
}
rafId.current = null;
});
}
}, [docId, user]);
// 2. DIRECT DOM INJECTION: Render remote cursors without touching React virtual DOM
const updateRemoteCursorDOM = (cursor: RemoteCursor) => {
if (!canvasOverlayRef.current) return;
let el = document.getElementById(`cursor-${cursor.id}`);
if (!el) {
el = document.createElement("div");
el.id = `cursor-${cursor.id}`;
el.className = "absolute pointer-events-none transition-transform duration-75 flex items-center gap-1";
el.innerHTML = `
<svg width="18" height="18" viewBox="0 0 24 24" fill="${cursor.color}">
<path d="M5.5 3.2L18.8 12l-7.3 1.8-3.7 6.4L5.5 3.2z"/>
</svg>
<span class="px-1.5 py-0.5 rounded text-[10px] font-bold text-white" style="background:${cursor.color}">
${cursor.name}
</span>
`;
canvasOverlayRef.current.appendChild(el);
}
// Hardware accelerated GPU translate avoids Layout / Reflow thrash
el.style.transform = `translate3d(${cursor.x}px, ${cursor.y}px, 0)`;
};
// 3. OPTIMISTIC UI MUTATION WITH AUTOMATIC ROLLBACK
const handleApplyTextMutation = async (newText: string) => {
const previousDoc = { ...doc };
const optimisticDoc: CanvasDocument = {
...doc,
content: newText,
version: doc.version + 1,
};
// Immediate UI feedback (0ms perceived latency)
setDoc(optimisticDoc);
setIsSyncing(true);
try {
socketRef.current?.send(`canvas:${docId}:mutation`, {
docId,
baseVersion: doc.version,
patch: newText,
});
} catch (err) {
// Rollback on network failure
console.error("Mutation rejected by network. Rolling back:", err);
setDoc(previousDoc);
} finally {
setIsSyncing(false);
}
};
useEffect(() => {
const socket = new ResilientWebSocket({ url: `wss://api.frontendprep.com/v1/realtime` });
socketRef.current = socket;
socket.connect();
// Subscribe to presence cursors
const unsubPresence = socket.subscribe<RemoteCursor>(`canvas:${docId}:presence`, (remoteData) => {
if (remoteData.id === user.id) return; // Skip self
cursorsRef.current.set(remoteData.id, remoteData);
updateRemoteCursorDOM(remoteData);
});
// Subscribe to document updates
const unsubDocs = socket.subscribe<CanvasDocument>(`canvas:${docId}:mutation`, (incomingDoc) => {
setDoc(incomingDoc);
});
return () => {
unsubPresence();
unsubDocs();
socket.disconnect();
if (rafId.current) cancelAnimationFrame(rafId.current);
};
}, [docId, user.id]);
return (
<div
className="relative w-full h-[600px] border border-card-border rounded-2xl bg-zinc-950 overflow-hidden"
onMouseMove={(e) => {
const rect = e.currentTarget.getBoundingClientRect();
broadcastCursor(e.clientX - rect.left, e.clientY - rect.top);
}}
>
{/* Direct DOM mount point for 60fps cursor rendering */}
<div ref={canvasOverlayRef} className="absolute inset-0 pointer-events-none z-50" />
{/* Canvas Workspace */}
<div className="p-8">
<div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-semibold text-foreground">Document: {doc.id} (v{doc.version})</h3>
<span className="text-xs px-2 py-0.5 rounded-full bg-emerald-500/10 text-emerald-400 border border-emerald-500/20">
{isSyncing ? "Syncing..." : "Connected"}
</span>
</div>
<textarea
value={doc.content}
onChange={(e) => handleApplyTextMutation(e.target.value)}
className="w-full h-80 bg-zinc-900 border border-zinc-800 rounded-xl p-4 text-foreground text-sm font-mono focus:outline-none focus:border-sky-500"
placeholder="Type here to test multi-user synchronization..."
/>
</div>
</div>
);
};9. Common Mistakes
Here are the critical traps that cause candidates to fail real-time system design interviews:
1. Opening One WebSocket Per Widget / Screen Component
Candidates frequently propose spinning up a new WebSocket inside each child component that needs live data.
- The Pitfall: Browsers enforce strict TCP connection limits (especially in HTTP/1.1 proxies). Opening 10 sockets per page exhausts client sockets, multiplies server TLS handshakes, and burns battery.
- The Correct Pattern: Use a single multiplexed connection via a Centralized Socket Provider / Event Bus. Individual widgets register channel filters on the singleton stream.
2. Relying on TCP onclose to Detect Network Dropouts
When a user walks into an elevator or their phone switches Wi-Fi to 5G, the OS does not transmit a TCP FIN or RST packet.
- The Pitfall: The client and server both believe the socket is open ("Half-Open / Zombie Socket"). Messages sent vanish into a black hole.
- The Correct Pattern: Application-level Ping/Pong Heartbeats. If no response frame arrives within a configured heartbeat timeout window, terminate the socket client-side and trigger reconnection.
3. Updating React State for Every Inbound Network Packet
- The Pitfall: In high-throughput streams (e.g., live stock ticker or multi-user cursor tracking), passing 100 updates/sec through
useStatecauses React to schedule hundreds of reconciliation cycles, freezing the browser. - The Correct Pattern:
- Direct hardware-accelerated DOM mutation via
transform: translate3d(...)for spatial cursors. - Or throttle inbound state updates using an accumulator buffer drained at 60 FPS using
requestAnimationFrame.
- Direct hardware-accelerated DOM mutation via
4. Trusting Client Timestamps for Ordering Events
- The Pitfall: A client whose system clock is 2 minutes slow sends a message with an earlier timestamp, corrupting chronological chat logs or document histories.
- The Correct Pattern: Use server-assigned monotonically increasing sequence numbers or distributed Vector Clocks (Lamport Timestamps).
5. Ignoring Background Tab Degradation
- The Pitfall: Inactive browser tabs keep consuming full-bandwidth WebSocket telemetry, draining mobile battery and keeping idle server sockets open.
- The Correct Pattern: Hook into the Page Visibility API (
document.visibilityState). Whenhidden, pause high-frequency subscriptions (like cursor movement) and fall back to low-frequency heartbeat pings.
10. Performance Considerations
1. Serialization: JSON vs. Binary Protocols (MessagePack / Protocol Buffers)
In high-frequency real-time systems, JSON serialization and string parsing become a major CPU bottleneck.
┌────────────────────────┬───────────────────┬──────────────────────┐
│ Payload Type │ Wire Size (Bytes) │ Parse Overhead (10k) │
├────────────────────────┼───────────────────┼──────────────────────┤
│ Verbose JSON │ 480 B │ ~18.4 ms │
│ Compact JSON │ 210 B │ ~9.2 ms │
│ MessagePack (Binary) │ 94 B │ ~2.1 ms │
│ Protobuf (Typed Binary)│ 46 B │ ~0.8 ms │
└────────────────────────┴───────────────────┴──────────────────────┘
[!TIP] In an interview, recommend JSON for lower-frequency metadata and chat, and switch to MessagePack or Protobuf for telemetry, collaborative cursor positions, and binary CRDT updates.
2. Offloading to Web Workers
For data-intensive apps (e.g., live financial trading desks or large document syncs), running socket deserialization, compression decompression (zlib), and CRDT tree calculations on the main thread will cause dropped frames and high INP.
Move the WebSocket and CRDT engine into a dedicated Web Worker:
[ Web Worker Thread ]
WebSocket Connection ──> Binary Parsing ──> CRDT Merge ──> Transferred ArrayBuffer
│
(postMessage 0ms copy)
▼
[ Main UI Thread ] ──────────────────────────────────> Direct WebGL / DOM Render
11. Best Practices
- Idempotency Keys on All Mutations: Every client-generated mutation must contain a unique UUID
v4(). If a network glitch causes the client to retransmit a queued message upon reconnecting, the server identifies the duplicate and drops it without applying it twice. - Deterministic Sequence Numbers: Messages should carry a zero-indexed sequence ID (
seq: 1402). Clients can immediately detect dropped packets (received 1404 without 1403) and request a backfill delta from the server. - Structured Connection States: Expose an observable finite state machine (FSM) to UI components:
IDLE→CONNECTING→CONNECTED→RECONNECTING→OFFLINE. Never leave the user in the dark when connectivity degrades. - Graceful Teardown in Component Cleanups: Always cleanly unsubscribe event listeners and release sockets during unmount to eliminate memory leaks across Next.js / React page navigations.
12. Production Recommendations
When deploying collaborative real-time apps at enterprise scale (millions of concurrent connections):
1. Scaling Beyond a Single Node with Redis Pub/Sub or NATS
A single Node.js or Go server can comfortably hold 50,000–100,000 idle TCP sockets. However, two users working on Document X may be connected to different physical servers:
User A ────────> [ Socket Server 1 ]
│
▼ (Publish to "doc:42")
[ Redis Cluster / NATS ]
▲
│ (Subscribed to "doc:42")
User B ────────> [ Socket Server 2 ]
2. Load Balancer Sticky Sessions
During initial HTTP upgrade handshakes, ensure Layer 7 load balancers (e.g., AWS ALB, Cloudflare, NGINX) use IP hash or session affinity cookie so the upgrade request lands on the exact same pod that served the initial handshake.
3. Graceful Fallback Strategy
Always implement a fallback hierarchy. If strict enterprise firewalls or corporate VPNs block WebSocket connections:
WebSocket (Primary) ──[Blocked]──> Server-Sent Events (SSE) ──[Blocked]──> HTTP Long-Polling
13. Summary & Key Takeaways
The 45-Minute Interview Cheat Sheet
When asked to "Design a Real-Time Collaborative System (Figma / Google Docs / Slack)", structure your answer using this step-by-step framework:
00-05m: Clarify Requirements
• Read/Write throughput, number of concurrent room collaborators
• Conflict resolution requirements (simple presence vs text vs canvas)
• Offline support expectations
05-15m: High-Level Architecture
• Protocol selection: WebSockets vs SSE vs WebTransport
• Client layers: Transport Manager, CRDT/OT Sync Engine, Optimistic Store
• Gateway topology: Load Balancer, Socket Pods, Redis Pub/Sub broker
15-30m: Deep Dive on Critical Components
• Resilient socket mechanics: Heartbeats, exponential backoff with jitter
• Conflict resolution: Compare OT (centralized) vs CRDT (decentralized)
• Optimistic UI with rollback on network rejection
30-40m: Performance & Edge Cases
• Throttling 120Hz presence events with requestAnimationFrame
• Bypassing React virtual DOM for cursors via direct DOM / WebGL updates
• Zombie socket prevention and Page Visibility API sleeping
• Offline outbox queue & idempotency deduplication
40-45m: Wrap-Up & Scaling
• Horizontal scaling via Redis Pub/Sub, NATS, and snapshot compaction
Core Decisions Matrix
- Choose Server-Sent Events (SSE) when the flow is strictly one-way (newsfeeds, AI chat completions, analytics dashboards).
- Choose WebSockets when low-latency bidirectional interaction is essential (chat rooms, live collaborative whiteboards, multiplayer editors).
- Choose CRDTs when you need offline-first local performance and peer-to-peer or serverless simplicity without a complex central lock.
- Choose Operational Transformation (OT) when minimal wire payload size is paramount and you maintain an authoritative central server.
