Project: Real-Time Application
📖 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:
- Low latency — drawing must feel instant; use WebSocket binary data
- State synchronization — new users joining must see current whiteboard state
- Conflict resolution — two users drawing simultaneously
- Bandwidth — throttle cursor/draw events (requestAnimationFrame, not per-pixel)
- 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
1// Real-Time Collaborative Whiteboard — Server23const express = require("express");4const { createServer } = require("http");5const { Server } = require("socket.io");67const app = express();8const server = createServer(app);9const io = new Server(server, {10 cors: { origin: "*" },11 maxHttpBufferSize: 1e6, // 1MB for binary drawing data12});1314// In-memory room state (production: use Redis + MongoDB)15const rooms = new Map();1617function 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}2829// Socket.IO connection30io.on("connection", (socket) => {31 let currentRoom = null;32 let username = null;3334 // Join a whiteboard room35 socket.on("room:join", ({ roomId, user }) => {36 currentRoom = roomId;37 username = user;38 socket.join(roomId);3940 const room = getRoom(roomId);41 room.users.set(socket.id, { username: user, cursor: null });4243 // Send current whiteboard state to the new user44 socket.emit("room:state", {45 strokes: room.strokes,46 users: Array.from(room.users.values()),47 });4849 // Notify others50 socket.to(roomId).emit("user:joined", { username: user, socketId: socket.id });5152 io.to(roomId).emit("user:list", Array.from(room.users.values()));53 });5455 // Drawing events56 socket.on("draw:stroke", (strokeData) => {57 if (!currentRoom) return;58 const room = getRoom(currentRoom);5960 // Store stroke for new joiners61 room.strokes.push(strokeData);6263 // Broadcast to everyone else in the room64 socket.to(currentRoom).emit("draw:stroke", strokeData);65 });6667 // 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 };7374 socket.to(currentRoom).emit("cursor:move", {75 socketId: socket.id,76 username,77 x, y,78 });79 });8081 // Undo82 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 });8889 // Clear board90 socket.on("draw:clear", () => {91 if (!currentRoom) return;92 const room = getRoom(currentRoom);93 room.strokes = [];94 io.to(currentRoom).emit("draw:clear");95 });9697 // Chat98 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 });106107 // Disconnect108 socket.on("disconnect", () => {109 if (currentRoom) {110 const room = getRoom(currentRoom);111 room.users.delete(socket.id);112113 socket.to(currentRoom).emit("user:left", { username, socketId: socket.id });114 io.to(currentRoom).emit("user:list", Array.from(room.users.values()));115116 // Clean up empty rooms117 if (room.users.size === 0) {118 rooms.delete(currentRoom);119 }120 }121 });122});123124app.use(express.static("public"));125126server.listen(3000, () => {127 console.log("Whiteboard server running on http://localhost:3000");128});
🏋️ Practice Exercise
Exercises:
- Build the collaborative whiteboard with rooms, freehand drawing, and real-time sync
- Add cursor tracking — show other users' cursors with their names
- Implement undo/redo that works across all connected clients
- Add a chat sidebar for room-level text communication
- Persist whiteboards to MongoDB — save automatically every 30 seconds and on room close
- 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.