Target Level: Senior Frontend Engineer / Staff Engineer
Duration: 45-60 minutes
Interview Focus: Real-time Collaboration, Conflict Resolution, State Synchronization, Performance
Interview Importance: π΄ Critical β Collaborative editing questions test state synchronization, concurrency control, offline recovery, and product UX trade-offs all at once, which makes them a classic senior frontend design problem.
Interview Approach & What Interviewers Look For
When asked to design Google Docs in a frontend interview, interviewers are evaluating:
- Real-time Collaboration: Can you design a system where multiple users can edit simultaneously without conflicts?
- Conflict Resolution: Do you understand Operational Transformation (OT) or CRDTs?
- State Management: How do you keep local and remote state synchronized?
- Performance: Can you handle large documents (100k+ characters) without lag?
- Offline Support: How do you queue changes when the user is offline?
- Scalability: Can your design support 50+ concurrent editors?
Pro Tip: Start by clarifying the scope, then dive into the collaborative editing algorithm (OT/CRDT), followed by architecture and implementation details.
1οΈβ£ Clarifying Questions (First 5 minutes)
Before diving in, ask these questions to scope the problem:
Functional Scope:
- "Should we support just text editing, or also rich formatting (bold, italics, images)?"
- "Do we need real-time cursor positions and presence indicators?"
- "Should we support comments and suggestions mode?"
- "Do we need version history and document recovery?"
Non-Functional Requirements:
- "How many concurrent users per document? 5? 50? 500?"
- "What's the target latency for edits to appear on other clients? <100ms?"
- "Should we support offline editing with sync when reconnected?"
- "What about conflict resolution when two users edit the same character?"
Technical Constraints:
- "Can we use WebSockets, or should we support HTTP-only environments?"
- "Browser support? Modern browsers only?"
- "Should we persist every keystroke, or batch updates?"
π Progressive Complexity Path
- π’ Junior: Define the editor surface, local document model, and why real-time sync cannot rely on page refreshes.
- π‘ Senior: Explain OT/CRDT trade-offs, pending operation queues, presence, and offline persistence.
- π΄ Staff: Cover conflict resolution at scale, operational telemetry, recovery workflows, and how to keep large collaborative documents responsive.
2οΈβ£ High-Level Architecture (Draw This!)
Loading diagramβ¦
Key Talking Points:
- Separation of Concerns: UI rendering is separate from document model and sync logic
- Optimistic Updates: Show changes immediately, reconcile in background
- Event-Driven: WebSocket pushes changes to all connected clients
- Persistence: Both client-side (IndexedDB) and server-side (Database)
3οΈβ£ Core Technical Decisions
3.1 Collaborative Editing: Operational Transformation (OT) vs CRDT
Interview Answer:
"For real-time collaboration, we need to handle concurrent edits from multiple users. There are two main approaches:
Operational Transformation (OT):
- β Smaller bandwidth (sends operations, not full state)
- β Google Docs uses this
- β Complex: requires transform functions for every operation pair
- β Central server required for operation ordering
Conflict-Free Replicated Data Types (CRDT):
- β Simpler: mathematically proven to converge
- β Works peer-to-peer (no central server)
- β Better offline support
- β Larger payload size (contains metadata)
- β Examples: Yjs, Automerge
For Google Docs scale, I'd use OT since it's bandwidth-efficient and we have a central server anyway."
3.2 Operational Transformation Explained
Interview Answer:
"OT transforms operations so they can be applied in any order and still converge to the same result. Here's a simple example:
Initial state:
'Hello'User A: Inserts
'!'at position 5 ->'Hello!'User B: Inserts'World'at position 5 ->'HelloWorld'When User A receives User B's operation:
- Original operation: Insert 'World' at position 5
- Transform: Since User A already inserted at position 5, we need to shift User B's operation
- Transformed operation: Insert 'World' at position 6
- Final state:
'Hello!World'Both users converge to the same state regardless of operation order."
Code Example:
class Operation { constructor(type, position, content) { this.type = type; // 'insert' or 'delete' this.position = position; this.content = content; this.clientId = null; this.version = null; this.id = Math.random().toString(36).slice(2, 11); // Unique operation ID } } // Transform operation B against operation A // Returns the transformed version of opB that accounts for opA being applied first const transform = (opA, opB) => { if (opA.type === 'insert' && opB.type === 'insert') { if (opA.position < opB.position) { // A inserts before B, shift B's position right return new Operation( opB.type, opB.position + opA.content.length, opB.content ); } else if (opA.position > opB.position) { // A inserts after B, B is unaffected return opB; } else { // Same position - use client ID for deterministic ordering if (opA.clientId < opB.clientId) { return new Operation( opB.type, opB.position + opA.content.length, opB.content ); } return opB; } } if (opA.type === 'delete' && opB.type === 'insert') { if (opB.position <= opA.position) { // B inserts before deletion, B is unaffected return opB; } else if (opB.position >= opA.position + opA.content.length) { // B inserts after deletion, shift B's position left return new Operation( opB.type, opB.position - opA.content.length, opB.content ); } else { // B inserts within deleted range, adjust to deletion point return new Operation( opB.type, opA.position, opB.content ); } } // Add more cases for delete-delete, etc. return opB; };
3.3 Document Model: ContentEditable vs Custom Rendering
Interview Answer:
"We have two options for rendering:
ContentEditable (Browser Native):
- β Built-in cursor, selection, input handling
- β Accessibility for free
- β Inconsistent across browsers
- β Hard to control (browser can insert
<span>,<div>, etc.)- β Difficult to reconcile with our document model
Custom Canvas Rendering:
- β Complete control over rendering
- β Consistent across browsers
- β Must implement cursor, selection, IME from scratch
- β Accessibility is complex
Google Docs uses a hybrid approach: ContentEditable for input capture, but the visual rendering is custom. They intercept mutations and convert to operations, then re-render from the document model."
Code Example:
class DocumentModel { constructor(initialText = '') { this.content = initialText; this.operations = []; // Operation log for OT this.version = 0; this.cursors = new Map(); // clientId -> position } applyOperation(operation) { switch(operation.type) { case 'insert': this.content = this.content.slice(0, operation.position) + operation.content + this.content.slice(operation.position); break; case 'delete': this.content = this.content.slice(0, operation.position) + this.content.slice(operation.position + operation.content.length); break; } this.operations.push(operation); this.version++; } getOperationsSince(version) { return this.operations.slice(version); } } // Example usage const doc = new DocumentModel('Hello'); // Local user types const op1 = new Operation('insert', 5, ' World'); doc.applyOperation(op1); console.log(doc.content); // 'Hello World' // Remote user's operation arrives const op2 = new Operation('insert', 0, 'Say '); doc.applyOperation(op2); console.log(doc.content); // 'Say Hello World'
4οΈβ£ Real-Time Synchronization Architecture
4.1 WebSocket Communication Protocol
Interview Answer:
"We need a reliable protocol for sending operations between clients and server:
Message Types:
OPERATION- Client sends edit operationOPERATION_ACK- Server acknowledges receipt (with server version)OPERATION_BROADCAST- Server sends operation to other clientsCURSOR_UPDATE- Client sends cursor positionPRESENCE_JOIN- User joins documentPRESENCE_LEAVE- User leaves documentSNAPSHOT- Full document state (for new clients)Flow:
- Client makes local edit -> Apply optimistically
- Send operation to server via WebSocket
- Server receives -> Transform against concurrent operations -> Persist -> Broadcast
- Other clients receive -> Transform against local pending operations -> Apply
- Original client receives ACK -> Commit operation (remove from pending queue)"
Code Example:
class SyncEngine { constructor(documentId, userId) { this.documentId = documentId; this.userId = userId; this.ws = null; this.doc = new DocumentModel(); this.pendingOps = []; // Operations not yet acknowledged this.serverVersion = 0; this.reconnectAttempts = 0; } connect() { this.ws = new WebSocket(`wss://api.example.com/docs/${this.documentId}`); this.ws.onopen = () => { console.log('Connected to document'); this.reconnectAttempts = 0; // Request initial snapshot this.ws.send(JSON.stringify({ type: 'SNAPSHOT_REQUEST', version: this.serverVersion })); }; this.ws.onmessage = (event) => { const message = JSON.parse(event.data); this.handleMessage(message); }; this.ws.onerror = (error) => { console.error('WebSocket error:', error); }; this.ws.onclose = () => { console.log('Disconnected from document'); this.reconnect(); }; } handleMessage(message) { switch(message.type) { case 'SNAPSHOT': this.doc = new DocumentModel(message.content); this.serverVersion = message.version; this.renderDocument(); break; case 'OPERATION_BROADCAST': this.handleRemoteOperation(message.operation); break; case 'OPERATION_ACK': this.handleAcknowledgement(message); break; case 'CURSOR_UPDATE': this.updateRemoteCursor(message.clientId, message.position); break; case 'PRESENCE_JOIN': this.handleUserJoin(message.user); break; case 'PRESENCE_LEAVE': this.handleUserLeave(message.userId); break; } } // User makes local edit handleLocalOperation(operation) { // 1. Apply optimistically to local document this.doc.applyOperation(operation); this.renderDocument(); // 2. Add to pending queue operation.clientId = this.userId; operation.baseVersion = this.serverVersion; this.pendingOps.push(operation); // 3. Send to server if (this.ws.readyState === WebSocket.OPEN) { this.ws.send(JSON.stringify({ type: 'OPERATION', operation: operation })); } else { // Offline - will sync when reconnected this.saveToOfflineQueue(operation); } } // Server broadcasts another user's operation handleRemoteOperation(operation) { // Transform against all pending local operations let transformed = operation; for (const pendingOp of this.pendingOps) { transformed = transform(pendingOp, transformed); } // Apply transformed operation this.doc.applyOperation(transformed); this.serverVersion++; this.renderDocument(); } // Server acknowledges our operation handleAcknowledgement(message) { const { operationId, version } = message; // Remove from pending queue this.pendingOps = this.pendingOps.filter( op => op.id !== operationId ); this.serverVersion = version; } reconnect() { this.reconnectAttempts++; const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000); console.log(`Reconnecting in ${delay}ms...`); setTimeout(() => this.connect(), delay); } renderDocument() { // Update editor display (implementation depends on UI framework) document.getElementById('editor').textContent = this.doc.content; } async getDB() { // Initialize and return IndexedDB connection if (this.db) return this.db; return new Promise((resolve, reject) => { const request = indexedDB.open('GoogleDocsOffline', 1); request.onerror = () => reject(request.error); request.onsuccess = () => { this.db = request.result; resolve(this.db); }; request.onupgradeneeded = (event) => { const db = event.target.result; if (!db.objectStoreNames.contains('operations')) { db.createObjectStore('operations', { autoIncrement: true }); } }; }); } async saveToOfflineQueue(operation) { // Store in IndexedDB for offline support const db = await this.getDB(); const tx = db.transaction(['operations'], 'readwrite'); tx.objectStore('operations').add({ documentId: this.documentId, operation: operation, timestamp: Date.now() }); } }
5οΈβ£ Common Interview Questions
Q1: When would you choose OT instead of CRDT for collaborative text editing?
Answer: Choose OT when you have a central coordination server, want smaller payloads, and need predictable ordering for large collaborative documents. CRDT is attractive when offline-first or peer-to-peer collaboration is the primary requirement, but it usually carries more metadata overhead.
Q2: How do you stop local typing from feeling laggy during poor network conditions?
Answer: Apply operations optimistically to the local model, queue them for sync, and reconcile later with ACKs plus transformation against remote operations. That keeps keystrokes immediate while preserving convergence once the server catches up.
Q3: What breaks first when the document becomes very large?
Answer: Rendering and cursor bookkeeping usually break before the sync algorithm does. You need virtualization, incremental layout work, and careful presence updates so the editor stays responsive even when the operation log keeps growing.
π Summary & Key Takeaways
What to emphasize:
- β Collaborative editing algorithm: Deep understanding of OT or CRDT
- β Real-time sync: WebSocket communication patterns
- β Conflict resolution: How to handle concurrent edits
- β Performance: Large document handling, batching, virtualization
- β Offline support: Queue operations, sync on reconnect
- β User experience: Cursor synchronization, presence, smooth editing
What to avoid:
- β Ignoring conflict resolution (biggest pitfall)
- β Not discussing OT/CRDT tradeoffs
- β Forgetting offline scenarios
- β Overlooking security (XSS, permissions)
- β Not considering performance at scale
- β Missing presence/awareness features
Sample closing statement:
"To summarize, I'd build a real-time collaborative editor using Operational Transformation for conflict-free editing, WebSocket for low-latency synchronization, and IndexedDB for offline support. The key challenges are maintaining consistency across clients, handling network partitions, and optimizing performance for large documents. I'd use virtualization for rendering, batching for network efficiency, and comprehensive monitoring to ensure <100ms sync latency. The architecture separates UI, document model, and sync engine for maintainability."
6οΈβ£ Common Pitfalls
- β Relying on last-write-wins without a real transform or merge strategy.
- β Treating WebSocket disconnects as rare instead of designing for offline queues and replay.
- β Re-rendering the full document on every remote operation instead of batching or virtualizing updates.
- β Forgetting presence, permissions, and XSS concerns when rendering collaborative rich text.
β±οΈ Complexity Summary
| Operation | Time Complexity | Space Complexity | Why it matters |
|---|---|---|---|
| Apply local insert/delete | O(n) | O(n) | Array or string-backed editors shift content as document size grows |
| Transform remote op against pending ops | O(p) | O(p) | p is the count of unacknowledged local operations |
| Rebuild visible editor slice | O(v) | O(v) | Virtualization keeps work proportional to visible content instead of full document size |
| Replay offline queue | O(q) | O(q) | Sync cost grows with queued operations after reconnect |
π Further Reading
-
Libraries:
- Yjs - CRDT implementation
- ShareDB - OT framework
- ProseMirror - Rich text editor with collaborative editing support
- Quill - Rich text editor
- Slate.js - Customizable rich text framework
-
Papers:
- "Operational Transformation in Real-Time Group Editors" (Ellis & Gibbs, 1989)
- "A Comprehensive Study of Convergent and Commutative Replicated Data Types" (Shapiro et al., 2011)
-
Open Source Examples:
Pro Tips for the Interview:
- Draw diagrams! Visual representation of OT/CRDT concepts helps interviewers follow your thought process
- Walk through concrete examples (e.g., "User A types 'X' at position 5 while User B deletes characters 3-7")
- Discuss tradeoffs explicitly (e.g., "OT has lower bandwidth but higher complexity")
- Mention real-world products (Google Docs uses OT, Notion uses CRDT)
- Be prepared to implement a simple OT transform function on a whiteboard
Good luck with your interview! π
Test your understanding with 3 quick questions