๐The Interview Setting
Company: Figma Round: Frontend System Design Duration: 60 minutes
๐๐ค Interviewer: "Design a collaborative whiteboard canvas."
Candidate's Clarifications:
Interviewer: "Basic shapes, up to 10 users real-time, yes to undo/redo and zoom/pan."
๐๐ Core Data Structures
// Shape base typeinterface Shape { id: string; type: 'rectangle' | 'circle' | 'text' | 'line'; x: number; y: number; width: number; height: number; rotation: number; fill: string; stroke: string; strokeWidth: number; zIndex: number; createdBy: string; createdAt: number;}// Canvas stateinterface CanvasState { shapes: Map<string, Shape>; selectedIds: Set<string>; viewport: { x: number; y: number; zoom: number }; history: HistoryStack;}
๐๐ค Interviewer: "How do you render thousands of shapes efficiently?"
Candidate: "Use Canvas API with dirty rectangle optimization..."
class CanvasRenderer { constructor(canvas) { this.canvas = canvas; this.ctx = canvas.getContext('2d'); this.shapes = new Map(); this.dirtyRegions = []; this.rafId = null; } // Only redraw changed areas markDirty(region) { this.dirtyRegions.push(region); this.scheduleRender(); } scheduleRender() { if (this.rafId) return; this.rafId = requestAnimationFrame(() => { this.render(); this.rafId = null; }); } render() { const { ctx, shapes, dirtyRegions } = this; if (dirtyRegions.length === 0) { // Full redraw ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); this.renderAllShapes(); } else { // Partial redraw - only dirty regions for (const region of dirtyRegions) { ctx.save(); ctx.beginPath(); ctx.rect(region.x, region.y, region.width, region.height); ctx.clip(); ctx.clearRect(region.x, region.y, region.width, region.height); this.renderShapesInRegion(region); ctx.restore(); } } this.dirtyRegions = []; } renderAllShapes() { // Sort by zIndex const sorted = [...this.shapes.values()].sort((a, b) => a.zIndex - b.zIndex); for (const shape of sorted) { this.renderShape(shape); } } renderShape(shape) { const { ctx } = this; ctx.save(); // Apply transformations ctx.translate(shape.x + shape.width / 2, shape.y + shape.height / 2); ctx.rotate((shape.rotation * Math.PI) / 180); ctx.translate(-shape.width / 2, -shape.height / 2); // Draw based on type switch (shape.type) { case 'rectangle': ctx.fillStyle = shape.fill; ctx.strokeStyle = shape.stroke; ctx.lineWidth = shape.strokeWidth; ctx.fillRect(0, 0, shape.width, shape.height); ctx.strokeRect(0, 0, shape.width, shape.height); break; case 'circle': ctx.beginPath(); ctx.ellipse( shape.width / 2, shape.height / 2, shape.width / 2, shape.height / 2, 0, 0, Math.PI * 2 ); ctx.fillStyle = shape.fill; ctx.fill(); ctx.strokeStyle = shape.stroke; ctx.stroke(); break; } ctx.restore(); }}
๐๐ค Interviewer: "How do you implement undo/redo?"
Candidate: "Command pattern with operation-based history..."
class HistoryManager { constructor() { this.undoStack = []; this.redoStack = []; this.maxSize = 100; } execute(command) { command.execute(); this.undoStack.push(command); this.redoStack = []; // Clear redo on new action // Limit history size if (this.undoStack.length > this.maxSize) { this.undoStack.shift(); } } undo() { if (this.undoStack.length === 0) return; const command = this.undoStack.pop(); command.undo(); this.redoStack.push(command); } redo() { if (this.redoStack.length === 0) return; const command = this.redoStack.pop(); command.execute(); this.undoStack.push(command); }}// Command implementationsclass MoveCommand { constructor(shapeId, fromPos, toPos, canvas) { this.shapeId = shapeId; this.fromPos = fromPos; this.toPos = toPos; this.canvas = canvas; } execute() { this.canvas.updateShape(this.shapeId, this.toPos); } undo() { this.canvas.updateShape(this.shapeId, this.fromPos); }}class CreateCommand { constructor(shape, canvas) { this.shape = shape; this.canvas = canvas; } execute() { this.canvas.addShape(this.shape); } undo() { this.canvas.removeShape(this.shape.id); }}
๐๐ค Interviewer: "How do you handle real-time collaboration?"
Candidate: "CRDT or Operational Transform for conflict resolution..."
// Using Yjs for CRDTimport * as Y from 'yjs';import { WebsocketProvider } from 'y-websocket';const setupCollaboration = (roomId) => { const ydoc = new Y.Doc(); const provider = new WebsocketProvider( 'wss://collab.figma.com', roomId, ydoc ); // Shared shapes map const yShapes = ydoc.getMap('shapes'); // Local to remote const updateShape = (shapeId, updates) => { const shape = yShapes.get(shapeId); yShapes.set(shapeId, { ...shape, ...updates }); }; // Remote to local yShapes.observe((event) => { event.changes.keys.forEach((change, key) => { if (change.action === 'add') { renderer.addShape(yShapes.get(key)); } else if (change.action === 'update') { renderer.updateShape(key, yShapes.get(key)); } else if (change.action === 'delete') { renderer.removeShape(key); } }); }); // Cursor awareness const awareness = provider.awareness; awareness.setLocalState({ user: { name: currentUser.name, color: currentUser.color }, cursor: null }); // Track other users' cursors awareness.on('change', () => { const states = awareness.getStates(); renderCursors(states); }); return { ydoc, yShapes, awareness };};
๐๐ค Interviewer: "What about zoom and pan?"
Candidate:
const useViewport = () => { const [viewport, setViewport] = useState({ x: 0, y: 0, zoom: 1 }); // Pan with middle mouse or space + drag const handlePan = (deltaX, deltaY) => { setViewport(v => ({ ...v, x: v.x - deltaX / v.zoom, y: v.y - deltaY / v.zoom })); }; // Zoom toward cursor position const handleZoom = (delta, cursorX, cursorY) => { setViewport(v => { const zoomFactor = delta > 0 ? 1.1 : 0.9; const newZoom = Math.min(Math.max(v.zoom * zoomFactor, 0.1), 10); // Zoom toward cursor const worldX = (cursorX - v.x) / v.zoom; const worldY = (cursorY - v.y) / v.zoom; return { x: cursorX - worldX * newZoom, y: cursorY - worldY * newZoom, zoom: newZoom }; }); }; // Convert screen coords to world coords const screenToWorld = (screenX, screenY) => ({ x: (screenX - viewport.x) / viewport.zoom, y: (screenY - viewport.y) / viewport.zoom }); return { viewport, handlePan, handleZoom, screenToWorld };};
๐๐ Interview Scoring
Result: Strong Hire ๐