Project: Real-Time Application

0/3 in this phase0/48 across the roadmap

📖 Concept

Build a real-time collaborative application that showcases WebSockets, pub/sub, and event-driven architecture.

Project: Real-Time Collaborative Whiteboard

Features:

  • Multiple users draw simultaneously on a shared canvas
  • Real-time cursor tracking for all connected users
  • Room-based sessions (create/join whiteboards)
  • Shape tools: freehand, line, rectangle, circle, text
  • Color picker, brush size, undo/redo
  • Chat sidebar for room communication
  • Export whiteboard as PNG/SVG
  • Persistent storage (save/load whiteboards)

Architecture:

React Frontend → Socket.IO Client
        ↕
nginx (WebSocket upgrade)
        ↕
Node.js → Socket.IO Server → Redis Adapter (for scaling)
        ↕
MongoDB (whiteboard persistence)

Key technical challenges:

  1. Low latency — drawing must feel instant; use WebSocket binary data
  2. State synchronization — new users joining must see current whiteboard state
  3. Conflict resolution — two users drawing simultaneously
  4. Bandwidth — throttle cursor/draw events (requestAnimationFrame, not per-pixel)
  5. Scaling — Redis adapter for multi-server WebSocket chat

This project is excellent for interviews because it demonstrates:

  • WebSocket mastery (bidirectional, rooms, binary data)
  • Event-driven architecture
  • State management across distributed clients
  • Performance optimization (throttling, batching)
  • Horizontal scaling strategies

💻 Code Example

codeTap to expand ⛶
1// Real-Time Collaborative Whiteboard — Server
2
3const express = require("express");
4const { createServer } = require("http");
5const { Server } = require("socket.io");
6
7const app = express();
8const server = createServer(app);
9const io = new Server(server, {
10 cors: { origin: "*" },
11 maxHttpBufferSize: 1e6, // 1MB for binary drawing data
12});
13
14// In-memory room state (production: use Redis + MongoDB)
15const rooms = new Map();
16
17function getRoom(roomId) {
18 if (!rooms.has(roomId)) {
19 rooms.set(roomId, {
20 id: roomId,
21 users: new Map(),
22 strokes: [],
23 createdAt: Date.now(),
24 });
25 }
26 return rooms.get(roomId);
27}
28
29// Socket.IO connection
30io.on("connection", (socket) => {
31 let currentRoom = null;
32 let username = null;
33
34 // Join a whiteboard room
35 socket.on("room:join", ({ roomId, user }) => {
36 currentRoom = roomId;
37 username = user;
38 socket.join(roomId);
39
40 const room = getRoom(roomId);
41 room.users.set(socket.id, { username: user, cursor: null });
42
43 // Send current whiteboard state to the new user
44 socket.emit("room:state", {
45 strokes: room.strokes,
46 users: Array.from(room.users.values()),
47 });
48
49 // Notify others
50 socket.to(roomId).emit("user:joined", { username: user, socketId: socket.id });
51
52 io.to(roomId).emit("user:list", Array.from(room.users.values()));
53 });
54
55 // Drawing events
56 socket.on("draw:stroke", (strokeData) => {
57 if (!currentRoom) return;
58 const room = getRoom(currentRoom);
59
60 // Store stroke for new joiners
61 room.strokes.push(strokeData);
62
63 // Broadcast to everyone else in the room
64 socket.to(currentRoom).emit("draw:stroke", strokeData);
65 });
66
67 // Cursor movement (throttled on client side)
68 socket.on("cursor:move", ({ x, y }) => {
69 if (!currentRoom) return;
70 const room = getRoom(currentRoom);
71 const user = room.users.get(socket.id);
72 if (user) user.cursor = { x, y };
73
74 socket.to(currentRoom).emit("cursor:move", {
75 socketId: socket.id,
76 username,
77 x, y,
78 });
79 });
80
81 // Undo
82 socket.on("draw:undo", () => {
83 if (!currentRoom) return;
84 const room = getRoom(currentRoom);
85 room.strokes.pop();
86 io.to(currentRoom).emit("draw:undo");
87 });
88
89 // Clear board
90 socket.on("draw:clear", () => {
91 if (!currentRoom) return;
92 const room = getRoom(currentRoom);
93 room.strokes = [];
94 io.to(currentRoom).emit("draw:clear");
95 });
96
97 // Chat
98 socket.on("chat:message", (text) => {
99 if (!currentRoom) return;
100 io.to(currentRoom).emit("chat:message", {
101 username,
102 text,
103 timestamp: Date.now(),
104 });
105 });
106
107 // Disconnect
108 socket.on("disconnect", () => {
109 if (currentRoom) {
110 const room = getRoom(currentRoom);
111 room.users.delete(socket.id);
112
113 socket.to(currentRoom).emit("user:left", { username, socketId: socket.id });
114 io.to(currentRoom).emit("user:list", Array.from(room.users.values()));
115
116 // Clean up empty rooms
117 if (room.users.size === 0) {
118 rooms.delete(currentRoom);
119 }
120 }
121 });
122});
123
124app.use(express.static("public"));
125
126server.listen(3000, () => {
127 console.log("Whiteboard server running on http://localhost:3000");
128});

🏋️ Practice Exercise

Exercises:

  1. Build the collaborative whiteboard with rooms, freehand drawing, and real-time sync
  2. Add cursor tracking — show other users' cursors with their names
  3. Implement undo/redo that works across all connected clients
  4. Add a chat sidebar for room-level text communication
  5. Persist whiteboards to MongoDB — save automatically every 30 seconds and on room close
  6. Scale to multiple servers using the Socket.IO Redis adapter

⚠️ Common Mistakes

  • Sending every mouse move event — this generates 60+ events/second per user; throttle to 20-30fps or use requestAnimationFrame

  • Not syncing state for late joiners — users who join after drawing has started see an empty canvas; send the full stroke history on join

  • Broadcasting to all sockets including the sender — the sender sees their own drawing immediately; broadcasting back causes double-rendering

  • Not handling reconnection — when a user's connection drops and reconnects, they should rejoin their room and receive the current state

  • Storing all drawing data in memory — for production, save strokes to a database periodically and load on demand

💼 Interview Questions

🎤 Mock Interview

Mock interview is powered by AI for Project: Real-Time Application. Login to unlock this feature.