Skip to main content

Collaborative Document Editor - Real-Time Editing System Design

Comprehensive low-level design for a real-time collaborative document editor with conflict resolution and sync strategies.

A collaborative document editor enables multiple users to simultaneously edit the same document while maintaining consistency and resolving conflicts in real-time through sophisticated synchronization mechanisms.

1. Problem Statement & Requirements

Clarifying questions

  1. What target devices/browsers should this collaborative document editor support? (e.g., mobile-first, desktop-only, specific browser version requirements)
  2. What authentication/authorization model is required? (e.g., OAuth, JWT, session-based, guest access)
  3. Is real-time collaboration a hard requirement or can we support eventual consistency with polling?
  4. What offline support is needed? (e.g., local editing with sync on reconnect, read-only offline mode)
  5. What accessibility level is required? (e.g., WCAG 2.1 AA, full keyboard navigation, screen reader support)
  6. Are i18n requirements part of this scope? (e.g., RTL support, multi-language content editing)
  7. What design system is available for this feature? (e.g., Material UI, custom design tokens, component library constraints)

Functional requirements

  • Real-time collaborative editing of rich text documents
  • Multiple users can edit simultaneously with visual indicators (e.g., cursors, avatars)
  • Version history and revert capability
  • Document sharing with different permission levels (view, edit, owner)
  • Rich text formatting (bold, italic, lists, headings, etc.)
  • Real-time presence indicators for connected users
  • Undo/redo functionality with collaborative awareness
  • Document search and navigation
  • User authentication and authorization

Non-functional requirements

  • Performance: Sub-500ms update latency for real-time collaboration
  • Scalability: Support 1000+ concurrent users per document
  • Accessibility: WCAG 2.1 AA compliance for all interactive elements
  • Reliability: 99.9% uptime with graceful degradation during network issues
  • Security: End-to-end encryption support for sensitive documents

Constraints and assumptions

  • Assumption: Users have stable internet connectivity for real-time collaboration
  • Constraint: Document size limited to 10MB (for performance and memory management)
  • Assumption: Content is stored in a centralized backend with proper versioning
  • Constraint: UI must be responsive across desktop and tablet form factors
  • Assumption: Users have modern browsers with support for WebSockets and ES2020 features

Out of scope

  • Full offline editing capability (local storage only with sync on reconnect)
  • Advanced document formatting (tables, embedded media, complex layouts)
  • Mobile app integration (web-only for initial release)
  • Advanced permission management (e.g., granular field-level permissions)
  • Document export to multiple formats (only HTML/JSON for MVP)
  • Mobile-specific optimizations (touch gestures, mobile keyboard handling)

2. Component Architecture

graph TD
    A[CollaborativeEditor] --> B[Toolbar]
    A --> C[EditorContainer]
    A --> D[UserList]
    A --> E[StatusBar]
    
    C --> F[EditorView]
    C --> G[EditorContent]
    C --> H[EditorCursor]
    
    F --> I[TextEditor]
    F --> J[FormattingToolbar]
    F --> K[CollaborationIndicator]
    
    I --> L[EditableContent]
    I --> M[LineNumbers]
    I --> N[SelectionManager]
    
    G --> O[DocumentRenderer]
    G --> P[VersionHistory]
    
    H --> Q[RemoteCursor]
    H --> R[LocalCursor]
    
    D --> S[UserAvatar]
    D --> T[UserStatus]

Component Tree Breakdown

Smart Components (Container Components):

  • CollaborativeEditor - Main container managing global state and user context
  • Toolbar - Manages toolbar actions and formatting controls
  • EditorContainer - Wraps editor content with collaboration features
  • UserList - Displays real-time user presence and status

Dumb Components (Presentational):

  • TextEditor - Renders the actual text editing surface
  • FormattingToolbar - Displays formatting options (bold, italic, etc.)
  • UserAvatar - Simple avatar display with status indicator
  • RemoteCursor - Visual representation of other users' cursors
  • StatusBar - Shows document metadata and sync status

TypeScript Props Contracts

// Main Editor Component
interface CollaborativeEditorProps {
  documentId: string;
  userId: string;
  initialContent?: string;
  readOnly?: boolean;
  onContentChange?: (content: string) => void;
  onUserJoin?: (user: User) => void;
  onUserLeave?: (userId: string) => void;
}

// Toolbar Component
interface ToolbarProps {
  isBold: boolean;
  isItalic: boolean;
  isUnderline: boolean;
  canUndo: boolean;
  canRedo: boolean;
  onBoldToggle: () => void;
  onItalicToggle:  () => void;
  onUnderlineToggle: () => void;
  onUndo: () => void;
  onRedo: () => void;
  onExport: () => void;
}

// User List Component
interface UserListProps {
  users: User[];
  currentUser: User;
  onUserClick: (user: User) => void;
}

// User Avatar Component
interface UserAvatarProps {
  user: User;
  size?: 'small' | 'medium' | 'large';
  showStatus?: boolean;
  onClick?: () => void;
}

// Editor Content Component
interface EditorContentProps {
  content: string;
  isEditable: boolean;
  onChange: (content: string) => void;
  onSelectionChange: (selection: SelectionRange) => void;
  cursorPosition: CursorPosition;
  remoteCursors: RemoteCursor[];
}

// Remote Cursor Component
interface RemoteCursorProps {
  cursor: RemoteCursor;
  color: string;
  name: string;
}

UI State Machine

stateDiagram-v2
    [*] --> Idle
    Idle --> Editing : user starts typing
    Editing --> Saving : content changes
    Saving --> Saved : successful save
    Saving --> Error : save failed
    Saved --> Idle : sync complete
    Error --> Idle : retry or user action
    
    [*] --> Offline
    Offline --> Online : network restored
    Online --> Idle : ready for interaction
    
    Editing --> Syncing : collaborative change
    Syncing --> Idle : sync complete

Component Responsibilities

CollaborativeEditor:

  • Manages global editor state and user context
  • Handles document lifecycle (load, save, sync)
  • Coordinates real-time collaboration features
  • Provides error boundaries and fallbacks

Toolbar:

  • Renders formatting controls
  • Manages toolbar state (active formats)
  • Handles user actions from toolbar buttons
  • Communicates with editor content component

EditorContainer:

  • Wraps the main editing surface
  • Manages editor context and state
  • Handles cursor and selection management
  • Integrates collaboration features

UserList:

  • Displays real-time user presence
  • Manages user status (online/offline)
  • Handles user interaction events
  • Provides user context information

Reusability Decisions

The component architecture prioritizes:

  1. Highly reusable: UserAvatar, RemoteCursor, FormattingToolbar - these can be used in any collaborative UI context
  2. Feature-specific: CollaborativeEditor, EditorContainer - tightly coupled to document editing workflow
  3. State management: Custom hooks for collaborative state that can be reused across different editor implementations

Component Lifecycle Considerations

  1. Mounting: Initialize WebSocket connections, fetch initial document state, register event listeners
  2. Updating: Handle content changes, user presence updates, collaboration events
  3. Unmounting: Clean up WebSocket connections, remove event listeners, save local state if needed
  4. Error Boundaries: Implement at the root of collaborative features to prevent cascading failures

The component design follows React best practices with clear separation of concerns, proper TypeScript typing, and state management patterns that support real-time collaboration features while maintaining performance and scalability.

3. State Management Design

Local state vs. global state decisions

For a collaborative document editor, we need to carefully distinguish between local and global state:

  • Local state: Component-specific UI state (e.g., active toolbar buttons, open modals, editor focus state)
  • Global state: Shared collaborative document content, user presence, real-time sync status, and configuration
  • Server state: Document versions, user permissions, and collaborative operations from the backend

State management approach

I recommend using Zustand as our primary state management solution for the following reasons:

  1. Simplicity: Minimal boilerplate and easy to understand
  2. Flexibility: Supports both local and global state patterns
  3. TypeScript native: Excellent type support with no additional configuration needed
  4. Small bundle size: ~1KB gzipped, ideal for collaborative editing
  5. React integration: First-class React support with hooks

For server state synchronization, we'll integrate React Query to handle document fetching, caching, and real-time updates.

State shape and normalization strategy

// Document state structure
interface DocumentState {
  id: string;
  title: string;
  content: string;
  version: number;
  lastModified: Date;
  collaborators: Collaborator[];
  isReadOnly: boolean;
  isSaving: boolean;
  saveError: string | null;
}

// Collaborator state
interface Collaborator {
  id: string;
  name: string;
  color: string;
  cursorPosition: CursorPosition | null;
  isActive: boolean;
  lastSeen: Date;
}

// Cursor position representation
interface CursorPosition {
  start: number;
  end: number;
  selectionText?: string;
}

// Real-time sync state
interface SyncState {
  status: 'idle' | 'syncing' | 'error';
  lastSynced: Date | null;
  pendingOperations: Operation[];
}

// Operation types for collaborative editing
type OperationType = 
  | 'INSERT_TEXT'
  | 'DELETE_TEXT'
  | 'FORMAT_TEXT'
  | 'MOVE_CURSOR'
  | 'ADD_COMMENT';

interface Operation {
  id: string;
  type: OperationType;
  userId: string;
  timestamp: Date;
  payload: any; // Specific to operation type
  version: number;
}

Server state vs. client state separation

Client state (Zustand):

  • User preferences and UI settings
  • Local document drafts
  • Active editor state (cursor, selection)
  • Collaborator presence and status

Server state (React Query):

  • Document content and metadata
  • Version history
  • User permissions and access control
  • Real-time collaborative operations

Cache invalidation strategy

  1. Document updates: Invalidate document cache when any user modifies content
  2. Version conflicts: When local operations conflict with server versions, trigger cache revalidation
  3. Collaborator changes: Update collaborator list cache on presence updates
  4. Manual invalidation: Clear cache when user explicitly refreshes or reloads

Optimistic updates design

For a collaborative editor, optimistic updates improve perceived performance:

  1. Local edits: Apply changes immediately to UI before server confirmation
  2. Conflict resolution: When server version differs, merge or prompt user for conflict resolution
  3. Operation queue: Maintain local operation queue to replay on reconnect
  4. User feedback: Show "syncing" indicator during optimistic update lifecycle

Critical-path hook implementation

// DocumentEditorStore.ts
import { create } from 'zustand';
import { useQuery, useQueryClient } from '@tanstack/react-query';

// Document state interface
interface DocumentState {
  id: string;
  title: string;
  content: string;
  version: number;
  lastModified: Date;
  collaborators: Collaborator[];
  isReadOnly: boolean;
  isSaving: boolean;
  saveError: string | null;
}

// Operation interface
interface Operation {
  id: string;
  type: 'INSERT_TEXT' | 'DELETE_TEXT' | 'FORMAT_TEXT';
  userId: string;
  timestamp: Date;
  payload: any;
  version: number;
}

// Collaborator interface
interface Collaborator {
  id: string;
  name: string;
  color: string;
  cursorPosition: { start: number; end: number } | null;
  isActive: boolean;
  lastSeen: Date;
}

// Zustand store for document editor
interface DocumentEditorStore {
  // Document state
  document: DocumentState | null;
  setDocument: (doc: DocumentState) => void;
  
  // Collaborator state
  collaborators: Collaborator[];
  setCollaborators: (collabs: Collaborator[]) => void;
  
  // Operation queue
  pendingOperations: Operation[];
  addPendingOperation: (op: Operation) => void;
  removePendingOperation: (id: string) => void;
  
  // UI state
  isReadOnly: boolean;
  setIsReadOnly: (readOnly: boolean) => void;
  isSaving: boolean;
  setIsSaving: (saving: boolean) => void;
  saveError: string | null;
  setSaveError: (error: string | null) => void;
  
  // Sync state
  syncStatus: 'idle' | 'syncing' | 'error';
  setSyncStatus: (status: 'idle' | 'syncing' | 'error') => void;
  
  // Actions
  updateContent: (content: string) => void;
  applyRemoteOperation: (operation: Operation) => void;
  saveDocument: () => Promise<void>;
}

// Create the document editor store
const useDocumentEditorStore = create<DocumentEditorStore>((set, get) => ({
  // Document state
  document: null,
  setDocument: (doc) => set({ document: doc }),
  
  // Collaborator state
  collaborators: [],
  setCollaborators: (collabs) => set({ collaborators: collabs }),
  
  // Operation queue
  pendingOperations: [],
  addPendingOperation: (op) => set((state) => ({
    pendingOperations: [...state.pendingOperations, op]
  })),
  removePendingOperation: (id) => set((state) => ({
    pendingOperations: state.pendingOperations.filter(op => op.id !== id)
  })),
  
  // UI state
  isReadOnly: false,
  setIsReadOnly: (readOnly) => set({ isReadOnly: readOnly }),
  isSaving: false,
  setIsSaving: (saving) => set({ isSaving: saving }),
  saveError: null,
  setSaveError: (error) => set({ saveError: error }),
  
  // Sync state
  syncStatus: 'idle',
  setSyncStatus: (status) => set({ syncStatus: status }),
  
  // Actions
  updateContent: (content) => {
    set((state) => {
      if (!state.document) return state;
      
      const newVersion = state.document.version + 1;
      const updatedDoc = {
        ...state.document,
        content,
        version: newVersion,
        lastModified: new Date()
      };
      
      return {
        document: updatedDoc,
        // Add to pending operations for optimistic update
        pendingOperations: [
          ...state.pendingOperations,
          {
            id: `op-${Date.now()}`,
            type: 'INSERT_TEXT',
            userId: 'current-user', // Would be from auth context
            timestamp: new Date(),
            payload: { content, version: newVersion },
            version: newVersion
          }
        ]
      };
    });
  },
  
  applyRemoteOperation: (operation) => {
    set((state) => {
      if (!state.document) return state;
      
      // Apply remote operation to document content
      const updatedContent = applyOperationToContent(
        state.document.content,
        operation
      );
      
      return {
        document: {
          ...state.document,
          content: updatedContent,
          version: operation.version
        }
      };
    });
  },
  
  saveDocument: async () => {
    const { document, setIsSaving, setSaveError, setSyncStatus } = get();
    
    if (!document) return;
    
    setIsSaving(true);
    setSyncStatus('syncing');
    
    try {
      // Simulate API call - in real implementation would use React Query
      await new Promise(resolve => setTimeout(resolve, 500));
      
      // In a real implementation:
      // const response = await fetch(`/api/documents/${document.id}`, {
      //   method: 'PUT',
      //   body: JSON.stringify({
      //     content: document.content,
      //     version: document.version
      //   })
      // });
      
      setSyncStatus('idle');
    } catch (error) {
      setSaveError('Failed to save document');
      setSyncStatus('error');
    } finally {
      setIsSaving(false);
    }
  }
}));

// Utility function to apply operation to content
const applyOperationToContent = (
  content: string,
  operation: Operation
): string => {
  switch (operation.type) {
    case 'INSERT_TEXT':
      return content + operation.payload.content;
    case 'DELETE_TEXT':
      // Simplified - real implementation would handle ranges
      return content.slice(0, -operation.payload.length);
    default:
      return content;
  }
};

// Custom hook for document operations
export const useDocumentEditor = () => {
  const store = useDocumentEditorStore();
  
  return {
    // Document state
    document: store.document,
    setDocument: store.setDocument,
    
    // Collaborator state
    collaborators: store.collaborators,
    setCollaborators: store.setCollaborators,
    
    // Operation queue
    pendingOperations: store.pendingOperations,
    addPendingOperation: store.addPendingOperation,
    removePendingOperation: store.removePendingOperation,
    
    // UI state
    isReadOnly: store.isReadOnly,
    setIsReadOnly: store.setIsReadOnly,
    isSaving: store.isSaving,
    setIsSaving: store.setIsSaving,
    saveError: store.saveError,
    setSaveError: store.setSaveError,
    
    // Sync state
    syncStatus: store.syncStatus,
    setSyncStatus: store.setSyncStatus,
    
    // Actions
    updateContent: store.updateContent,
    applyRemoteOperation: store.applyRemoteOperation,
    saveDocument: store.saveDocument
  };
};

// React Query integration for document fetching and caching
export const useDocumentQuery = (documentId: string) => {
  const queryClient = useQueryClient();
  
  return useQuery({
    queryKey: ['documents', documentId],
    queryFn: async () => {
      // Simulate API fetch
      const response = await fetch(`/api/documents/${documentId}`);
      return response.json();
    },
    staleTime: 5 * 60 * 1000, // 5 minutes
    cacheTime: 10 * 60 * 1000, // 10 minutes
    retry: 3,
    retryDelay: 1000,
    
    // Handle optimistic updates
    onSettled: (data, error) => {
      if (!error) {
        // Invalidate related queries when document changes
        queryClient.invalidateQueries({ queryKey: ['documents', documentId] });
      }
    }
  });
};

export default useDocumentEditorStore;

This implementation provides:

  1. Type-safe state management with proper TypeScript interfaces
  2. Optimistic updates for responsive UI
  3. Operation queueing to handle concurrent edits
  4. Sync status tracking for user feedback
  5. Error handling with proper state management
  6. Separation of concerns between local UI and global document state
  7. Scalable architecture that can handle real-time collaborative editing patterns

4. Routing & Navigation

Route Structure and Hierarchy

graph TD
    A[Root] --> B[Public Routes]
    A --> C[Protected Routes]
    B --> D[/login]
    B --> E[/register]
    B --> F[/forgot-password]
    C --> G[/documents]
    C --> H[/documents/:id]
    C --> I[/documents/:id/edit]
    G --> J[/documents/new]
    H --> K[/documents/:id/view]
    H --> L[/documents/:id/edit]

The application will have a clear route hierarchy:

  • Public Routes: /login, /register, /forgot-password - accessible without authentication
  • Protected Routes: All document-related routes require authentication and proper permissions
  • Nested Document Routes: /documents/:id serves as the base for view/edit modes, with /new for creating new documents

Code Splitting Strategy

Route-based code splitting with dynamic imports will be implemented:

// routes/documents.tsx
const DocumentList = React.lazy(() => import('features/documents/DocumentList'));
const DocumentEditor = React.lazy(() => import('features/documents/DocumentEditor'));
const DocumentViewer = React.lazy(() => import('features/documents/DocumentViewer'));

// app.tsx
const routes = createRoutesFromElements(
  <Route path="/" element={<Layout />}>
    <Route index element={<Navigate to="/documents" replace />} />
    
    {/* Public Routes */}
    <Route path="login" element={<Login />} />
    <Route path="register" element={<Register />} />
    
    {/* Protected Routes */}
    <Route path="documents" element={<ProtectedRoute />}>
      <Route index element={
        <Suspense fallback={<LoadingSpinner />}>
          <DocumentList />
        </Suspense>
      } />
      <Route path="new" element={
        <Suspense fallback={<LoadingSpinner />}>
          <DocumentEditor />
        </Suspense>
      } />
      <Route path=":id" element={
        <Suspense fallback={<LoadingSpinner />}>
          <DocumentViewer />
        </Suspense>
      } />
      <Route path=":id/edit" element={
        <Suspense fallback={<LoadingSpinner />}>
          <DocumentEditor />
        </Suspense>
      } />
    </Route>
  </Route>
);

Justification: Route-based splitting ensures that the document editor and its dependencies are only loaded when needed, reducing initial bundle size. The DocumentEditor component is particularly large due to its rich text capabilities and collaborative features.

Authentication guards will be implemented using React Router's useNavigate hook:

// components/ProtectedRoute.tsx
const ProtectedRoute = () => {
  const navigate = useNavigate();
  const { isAuthenticated, loading } = useAuth();

  useEffect(() => {
    if (!loading && !isAuthenticated) {
      navigate('/login', { replace: true });
    }
  }, [isAuthenticated, loading, navigate]);

  if (loading) return <LoadingSpinner />;
  
  return isAuthenticated ? <Outlet /> : null;
};

// components/DocumentEditor.tsx
const DocumentEditor = () => {
  const { documentId } = useParams();
  const navigate = useNavigate();
  
  useEffect(() => {
    // Check if user has permission to edit this document
    const checkDocumentAccess = async () => {
      try {
        const permissions = await fetchDocumentPermissions(documentId);
        if (!permissions.canEdit) {
          navigate(`/documents/${documentId}`, { replace: true });
        }
      } catch (error) {
        navigate('/documents', { replace: true });
      }
    };
    
    checkDocumentAccess();
  }, [documentId, navigate]);
};

Deep Linking Support and URL Parameter Design

URL structure will support deep linking with the following patterns:

/documents/:id          // View mode (default)
/documents/:id/edit     // Edit mode
/documents/new          // Create new document

URL parameters will be normalized and validated:

// utils/urlUtils.ts
export const validateDocumentId = (id: string): boolean => {
  return /^[a-zA-Z0-9-_]+$/.test(id) && id.length >= 10;
};

export const buildDocumentUrl = (id: string, mode: 'view' | 'edit' = 'view') => {
  if (mode === 'edit') {
    return `/documents/${id}/edit`;
  }
  return `/documents/${id}`;
};

Browser History Management and Scroll Restoration

React Router's built-in scroll restoration will be leveraged:

// app.tsx
const router = createBrowserRouter(routes, {
  future: {
    v7_normalizeFormMethod: true,
    v7_relativeSplatPath: true,
  },
  // Enable scroll restoration for better UX
  scrollRestoration: true,
});

// Custom scroll behavior for document editor
const handleScrollRestore = () => {
  const savedPosition = sessionStorage.getItem('scrollPosition');
  if (savedPosition) {
    window.scrollTo(0, parseInt(savedPosition));
  }
};

// In DocumentEditor component
useEffect(() => {
  const handleBeforeUnload = () => {
    sessionStorage.setItem('scrollPosition', window.scrollY.toString());
  };

  window.addEventListener('beforeunload', handleBeforeUnload);
  
  return () => {
    window.removeEventListener('beforeunload', handleBeforeUnload);
  };
}, []);

The scroll restoration will preserve user position when navigating between document views and editing modes, providing a seamless experience for users who frequently switch between different document states.

5. API Integration Layer

API Client Design Pattern

We'll implement a centralized API client using Axios with a custom fetch wrapper for better type safety and error handling. The client will be built as a singleton with request/response interceptors to handle authentication, error normalization, and automatic retries.

// api/client.ts
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';

class ApiClient {
  private client: AxiosInstance;

  constructor() {
    this.client = axios.create({
      baseURL: process.env.NEXT_PUBLIC_API_BASE_URL || 'https://api.example.com',
      timeout: 10000,
      headers: {
        'Content-Type': 'application/json',
      },
    });

    this.setupInterceptors();
  }

  private setupInterceptors() {
    // Request interceptor for auth token injection
    this.client.interceptors.request.use(
      (config) => {
        const token = localStorage.getItem('authToken');
        if (token) {
          config.headers.Authorization = `Bearer ${token}`;
        }
        return config;
      },
      (error) => Promise.reject(error)
    );

    // Response interceptor for error handling and normalization
    this.client.interceptors.response.use(
      (response) => response,
      (error) => {
        // Handle authentication errors
        if (error.response?.status === 401) {
          // Redirect to login or trigger auth flow
          localStorage.removeItem('authToken');
          window.location.href = '/login';
        }
        
        // Normalize error responses
        const normalizedError = {
          message: error.response?.data?.message || 'An unexpected error occurred',
          status: error.response?.status,
          code: error.response?.data?.code,
        };
        
        return Promise.reject(normalizedError);
      }
    );
  }

  public get<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
    return this.client.get<T>(url, config);
  }

  public post<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
    return this.client.post<T>(url, data, config);
  }

  public put<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
    return this.client.put<T>(url, data, config);
  }

  public delete<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
    return this.client.delete<T>(url, config);
  }
}

export const apiClient = new ApiClient();

Request/Response Interceptors

The interceptors handle:

  • Authentication token injection via Authorization header
  • Error normalization with consistent error object structure
  • Automatic redirect to login on 401 responses
  • Response data transformation and validation

Error Handling and Retry Strategy

We implement a retry mechanism with exponential backoff for transient network errors:

// api/retry.ts
export const retryWithBackoff = async <T>(
  fn: () => Promise<T>,
  maxRetries = 3,
  baseDelay = 1000
): Promise<T> => {
  let lastError: Error;
  
  for (let i = 0; i <= maxRetries; i++) {
    try {
      return await fn();
    } catch (error) {
      lastError = error as Error;
      
      // Don't retry on client errors (4xx)
      if (error.status && error.status < 500) {
        throw error;
      }
      
      // Retry on server errors (5xx) or network issues
      if (i < maxRetries) {
        const delay = baseDelay * Math.pow(2, i);
        await new Promise(resolve => setTimeout(resolve, delay));
        continue;
      }
    }
  }
  
  throw lastError;
};

Loading, Error, and Empty States Design

We implement a unified state management approach for API operations:

// api/types.ts
export type ApiState<T> = 
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: ApiError };

export interface ApiError {
  message: string;
  status?: number;
  code?: string;
}

// api/hooks/useApi.ts
import { useState, useEffect } from 'react';

export function useApi<T>(fn: () => Promise<T>) {
  const [state, setState] = useState<ApiState<T>>({ status: 'idle' });
  
  useEffect(() => {
    const fetchData = async () => {
      try {
        setState({ status: 'loading' });
        const data = await fn();
        setState({ status: 'success', data });
      } catch (error) {
        setState({
          status: 'error',
          error: error as ApiError,
        });
      }
    };
    
    fetchData();
  }, [fn]);
  
  return state;
}

Data Fetching Strategy

For collaborative document editing, we use a hybrid approach:

  • Server-Side Rendering (SSR) for initial document load to improve perceived performance
  • Client-Side Rendering (CSR) for real-time collaborative editing
  • Server-Side Generation (SSG) for static document templates or read-only views
// pages/document/[id].tsx (SSR example)
export async function getServerSideProps({ params }) {
  try {
    const document = await apiClient.get<Document>(`/documents/${params.id}`);
    
    return {
      props: {
        initialDocument: document,
      },
    };
  } catch (error) {
    return {
      notFound: true,
    };
  }
}

API Response Transformation Layer

We implement a DTO-to-domain model mapping layer to normalize data:

// api/mappers.ts
interface DocumentDto {
  id: string;
  title: string;
  content: string;
  version: number;
  lastModified: string;
  collaborators: CollaboratorDto[];
}

interface CollaboratorDto {
  id: string;
  name: string;
  email: string;
  color: string;
}

interface Document {
  id: string;
  title: string;
  content: string;
  version: number;
  lastModified: Date;
  collaborators: Collaborator[];
}

interface Collaborator {
  id: string;
  name: string;
  email: string;
  color: string;
}

export function mapDocumentDtoToDomain(dto: DocumentDto): Document {
  return {
    id: dto.id,
    title: dto.title,
    content: dto.content,
    version: dto.version,
    lastModified: new Date(dto.lastModified),
    collaborators: dto.collaborators.map(mapCollaboratorDtoToDomain),
  };
}

export function mapCollaboratorDtoToDomain(dto: CollaboratorDto): Collaborator {
  return {
    id: dto.id,
    name: dto.name,
    email: dto.email,
    color: dto.color,
  };
}

Sequence Diagram for Primary Data Flows

sequenceDiagram
    participant User
    participant Editor
    participant ApiClient
    participant WebSocket
    participant Server

    User->>Editor: Edit content
    Editor->>ApiClient: Save changes (POST /documents/:id/changes)
    ApiClient->>Server: Send change request
    Server->>WebSocket: Broadcast to collaborators
    WebSocket->>Editor: Receive update
    Editor->>Editor: Update local state
    Editor->>User: Show updated content

The sequence diagram shows the collaborative editing flow where:

  1. User makes an edit in the document
  2. Client sends change to server via POST request
  3. Server broadcasts the change through WebSocket to all collaborators
  4. Each collaborator receives the update and refreshes their local state
  5. UI updates to reflect the new content

This design ensures real-time collaboration while maintaining proper error handling, authentication, and data consistency across all clients.

6. Performance Design

Core Web Vitals Targets

  • Largest Contentful Paint (LCP): <2.5s for document content loading
  • Interaction to Next Paint (INP): <200ms for user interactions
  • Cumulative Layout Shift (CLS): <0.1 for stable layout during rendering

Bundle Size Budget per Route

  • Document Editor Route: <500KB (including all dependencies, editor core, and collaborative features)
  • User List/Presence Route: <100KB
  • Comments/Annotations Route: <150KB

Critical Rendering Path Optimization

// Critical CSS extraction and inlining for editor container
const criticalStyles = `
  .editor-container {
    height: 100vh;
    overflow: hidden;
    position: relative;
  }
  
  .editor-content {
    height: calc(100vh - 60px);
    overflow-y: auto;
    padding: 20px;
  }
  
  .editor-toolbar {
    height: 60px;
    border-bottom: 1px solid #e5e5e5;
    background: white;
    position: sticky;
    top: 0;
    z-index: 10;
  }
`;

Image Optimization Strategy

  • Formats: WebP for modern browsers, JPEG fallback for legacy
  • Lazy Loading: loading="lazy" attribute on all images
  • Responsive Images: srcset with multiple sizes for different viewports
  • Image Compression: WebP compression with 85% quality

Font Loading Strategy

// Font preloading strategy for editor
const fontPreload = [
  { href: '/fonts/inter-regular.woff2', as: 'font', type: 'font/woff2' },
  { href: '/fonts/inter-bold.woff2', as: 'font', type: 'font/woff2' }
];

// Critical font subset loading
const criticalFontSubset = `
  @font-face {
    font-family: 'Inter';
    src: url('/fonts/inter-critical.woff2') format('woff2');
    font-display: swap;
  }
`;

Memoization Strategy

// Memoize expensive content rendering operations
const MemoizedEditorContent = React.memo(({ content, onChange }) => {
  const processedContent = useMemo(() => {
    return processDocumentContent(content);
  }, [content]);

  const handleContentChange = useCallback((newContent) => {
    onChange(newContent);
  }, [onChange]);

  return (
    <div className="editor-content">
      {processedContent.map((block, index) => (
        <EditorBlock 
          key={index} 
          block={block} 
          onChange={handleContentChange}
        />
      ))}
    </div>
  );
});

// Memoize user presence updates
const UserPresenceItem = React.memo(({ user, isActive }) => {
  const avatarUrl = useMemo(() => generateAvatarUrl(user.id), [user.id]);
  
  return (
    <div className="user-presence-item">
      <img src={avatarUrl} alt={user.name} />
      <span className={isActive ? 'active' : 'inactive'}>
        {user.name}
      </span>
    </div>
  );
});

Virtualization for Large Lists

// Virtualized user list component using react-window
import { FixedSizeList as List } from 'react-window';

const VirtualizedUserList = ({ users, onUserSelect }) => {
  const Row = ({ index, style }) => (
    <div style={style} className="user-row">
      <UserPresenceItem 
        user={users[index]} 
        onSelect={() => onUserSelect(users[index])}
      />
    </div>
  );

  return (
    <List
      height={300}
      itemCount={users.length}
      itemSize={50}
      width="100%"
    >
      {Row}
    </List>
  );
};

Prefetching and Preloading Strategy

// Prefetch strategy for collaborative document loading
const DocumentPrefetcher = ({ docId }) => {
  useEffect(() => {
    // Preload critical document chunks
    const prefetchChunks = [
      `/api/documents/${docId}/metadata`,
      `/api/documents/${docId}/content?chunk=1`,
      `/api/documents/${docId}/comments`
    ];

    prefetchChunks.forEach(url => {
      fetch(url, { method: 'GET', keepalive: true });
    });

    // Preload user avatars for collaborators
    const preloadUserAvatars = (users) => {
      users.forEach(user => {
        if (user.avatarUrl) {
          const link = document.createElement('link');
          link.rel = 'prefetch';
          link.href = user.avatarUrl;
          document.head.appendChild(link);
        }
      });
    };

    return () => {
      // Cleanup prefetch links
      const prefetchLinks = document.querySelectorAll('link[rel="prefetch"]');
      prefetchLinks.forEach(link => link.remove());
    };
  }, [docId]);

  return null;
};

Performance Monitoring

// Performance monitoring for editor interactions
const useEditorPerformance = () => {
  const [metrics, setMetrics] = useState({
    renderTime: 0,
    inputDelay: 0,
    contentSyncLatency: 0
  });

  const measureRenderTime = (callback) => {
    const start = performance.now();
    const result = callback();
    const end = performance.now();
    
    setMetrics(prev => ({
      ...prev,
      renderTime: end - start
    }));
    
    return result;
  };

  const trackInputDelay = () => {
    // Track INP using PerformanceObserver
    const observer = new PerformanceObserver((list) => {
      list.getEntries().forEach(entry => {
        if (entry.entryType === 'first-input') {
          setMetrics(prev => ({
            ...prev,
            inputDelay: entry.processingStart - entry.startTime
          }));
        }
      });
    });

    observer.observe({ entryTypes: ['first-input'] });
  };

  return { metrics, measureRenderTime, trackInputDelay };
};

7. Error Handling & Resilience

Error Boundary Placement Strategy

Error boundaries should be strategically placed to provide the best user experience while maintaining system stability. The primary placement strategy includes:

  1. Document Editor Boundary: Wrap the main editor component with an error boundary to catch rendering issues in content areas
  2. Toolbar & Menu Boundary: Isolate toolbar components to prevent cascading failures from menu item errors
  3. User List Boundary: Wrap collaborative user list to prevent network issues from breaking the entire UI
  4. Save State Boundary: Wrap autosave and sync logic to prevent sync failures from crashing the editor
graph TD
    A[App] --> B[ErrorBoundary<DocumentEditor>]
    A --> C[ErrorBoundary<Toolbar>]
    A --> D[ErrorBoundary<UserList>]
    B --> E[DocumentEditor]
    C --> F[Toolbar]
    D --> G[UserList]
    E --> H[EditorContent]
    F --> I[MenuItems]
    G --> J[UserListItem]

Global Error Handler Design

A centralized error handler will capture unhandled errors and provide appropriate user feedback:

// src/core/error/GlobalErrorHandler.tsx
import React from 'react';

interface GlobalErrorState {
  hasError: boolean;
  error?: Error;
  errorInfo?: React.ErrorInfo;
}

class GlobalErrorHandler extends React.Component<{}, GlobalErrorState> {
  constructor(props: {}) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error: Error): GlobalErrorState {
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
    console.error('Global error caught:', error, errorInfo);
    // Report to monitoring service
    this.reportError(error, errorInfo);
  }

  private reportError = (error: Error, errorInfo: React.ErrorInfo) => {
    // Send to Sentry or similar monitoring service
    if (window.Sentry) {
      window.Sentry.captureException(error, {
        contexts: { react: errorInfo }
      });
    }
  };

  render() {
    if (this.state.hasError) {
      return (
        <div className="error-container">
          <h2>Something went wrong.</h2>
          <p>We're working to fix this issue. Please try again later.</p>
          <button onClick={() => window.location.reload()}>
            Reload Page
          </button>
        </div>
      );
    }

    return this.props.children;
  }
}

export default GlobalErrorHandler;

Offline / Network Failure Handling

The collaborative editor must gracefully handle network interruptions:

// src/core/network/NetworkManager.ts
import { useEffect, useState } from 'react';

interface NetworkStatus {
  isOnline: boolean;
  lastSyncTime?: Date;
  pendingChanges: number;
}

export const useNetworkStatus = () => {
  const [networkStatus, setNetworkStatus] = useState<NetworkStatus>({
    isOnline: navigator.onLine,
    pendingChanges: 0
  });

  useEffect(() => {
    const handleOnline = () => {
      setNetworkStatus(prev => ({
        ...prev,
        isOnline: true
      }));
      // Attempt to sync pending changes
      attemptSync();
    };

    const handleOffline = () => {
      setNetworkStatus(prev => ({
        ...prev,
        isOnline: false
      }));
    };

    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);

    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []);

  const attemptSync = async () => {
    // Retry pending changes with exponential backoff
    if (networkStatus.pendingChanges > 0) {
      // Implementation for retry logic with backoff
    }
  };

  return networkStatus;
};

Graceful Degradation Patterns

When features are unavailable, the editor should degrade gracefully:

// src/features/editor/EditorFallback.tsx
import React from 'react';

interface EditorFallbackProps {
  error?: Error;
  isOffline?: boolean;
  canEdit?: boolean;
}

const EditorFallback: React.FC<EditorFallbackProps> = ({ 
  error, 
  isOffline = false,
  canEdit = false 
}) => {
  if (isOffline) {
    return (
      <div className="editor-offline">
        <h3>Editor is offline</h3>
        <p>You're currently viewing the document in read-only mode.</p>
        <p>Changes will be saved when you reconnect.</p>
      </div>
    );
  }

  if (error) {
    return (
      <div className="editor-error">
        <h3>Editor Error</h3>
        <p>There was an issue loading the document.</p>
        <button onClick={() => window.location.reload()}>
          Reload Editor
        </button>
      </div>
    );
  }

  if (!canEdit) {
    return (
      <div className="editor-read-only">
        <h3>Read-only Mode</h3>
        <p>Collaboration features are temporarily unavailable.</p>
      </div>
    );
  }

  return null;
};

User Feedback on Errors

Comprehensive error feedback using toast notifications and inline indicators:

// src/core/ui/ToastManager.tsx
import React from 'react';
import { ToastContainer, toast } from 'react-toastify';

interface ToastOptions {
  type?: 'success' | 'error' | 'info' | 'warning';
  autoClose?: number;
  position?: 'top-right' | 'bottom-right' | 'top-center';
}

export const ToastManager = () => (
  <ToastContainer 
    position="top-right"
    autoClose={5000}
    hideProgressBar={false}
    newestOnTop={false}
    closeOnClick
    rtl={false}
    pauseOnFocusLoss
    draggable
    pauseOnHover
  />
);

export const showEditorError = (message: string, options?: ToastOptions) => {
  toast.error(message, {
    position: "top-right",
    autoClose: 5000,
    hideProgressBar: false,
    closeOnClick: true,
    pauseOnHover: true,
    draggable: true,
    ...options
  });
};

export const showSyncError = (message: string) => {
  toast.error(message, {
    position: "top-center",
    autoClose: false,
    closeOnClick: false,
    pauseOnHover: true,
    draggable: true
  });
};

export const showSyncSuccess = (message: string) => {
  toast.success(message, {
    position: "top-center",
    autoClose: 2000
    // ... other options
  });
};

Retry Strategy Implementation

For API calls and sync operations, implement a robust retry mechanism:

// src/core/network/RetryHandler.ts
interface RetryConfig {
  maxRetries?: number;
  baseDelay?: number;
  exponentialBase?: number;
}

export const withRetry = async <T>(
  operation: () => Promise<T>,
  config: RetryConfig = {}
): Promise<T> => {
  const {
    maxRetries = 3,
    baseDelay = 1000,
    exponentialBase = 2
  } = config;

  let lastError: Error;
  
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      return await operation();
    } catch (error) {
      lastError = error as Error;
      
      if (attempt === maxRetries) {
        throw lastError;
      }
      
      // Exponential backoff with jitter
      const delay = baseDelay * Math.pow(exponentialBase, attempt);
      const jitter = Math.random() * delay;
      
      await new Promise(resolve => setTimeout(resolve, delay + jitter));
    }
  }
  
  throw lastError!;
};

Error Recovery Patterns

Implement automatic recovery for common error states:

// src/features/editor/EditorRecovery.tsx
import React, { useEffect } from 'react';

interface RecoveryState {
  recoveryInProgress: boolean;
  recoveryFailed: boolean;
}

export const useEditorRecovery = (editorRef: React.RefObject<HTMLDivElement>) => {
  const [recoveryState, setRecoveryState] = useState<RecoveryState>({
    recoveryInProgress: false,
    recoveryFailed: false
  });

  const attemptRecovery = async () => {
    setRecoveryState(prev => ({ ...prev, recoveryInProgress: true }));
    
    try {
      // Attempt to restore from local storage
      const savedContent = localStorage.getItem('editor-content');
      if (savedContent && editorRef.current) {
        // Restore content
        editorRef.current.innerHTML = savedContent;
      }
      
      // Attempt to reconnect to collaboration service
      await reconnectToCollaboration();
      
      setRecoveryState({
        recoveryInProgress: false,
        recoveryFailed: false
      });
    } catch (error) {
      setRecoveryState({
        recoveryInProgress: false,
        recoveryFailed: true
      });
      console.error('Recovery failed:', error);
    }
  };

  const reconnectToCollaboration = async () => {
    // Implementation to reestablish collaboration connection
  };

  return { recoveryState, attemptRecovery };
};

8. Testing Strategy

Unit Testing Approach

Component Tests (70% coverage target):

  • Test all interactive components with various prop combinations
  • Verify rendering behavior for empty, loading, and error states
  • Test keyboard navigation and accessibility attributes
  • Mock external dependencies (API calls, browser APIs)
// Example: DocumentEditor component test
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { DocumentEditor } from './DocumentEditor';

describe('DocumentEditor', () => {
  const mockDocument = {
    id: 'doc-123',
    content: 'Hello world',
    collaborators: ['user-1', 'user-2'],
    version: 1
  };

  it('renders document content correctly', () => {
    render(<DocumentEditor document={mockDocument} />);
    expect(screen.getByText('Hello world')).toBeInTheDocument();
  });

  it('handles real-time collaboration updates', async () => {
    const user = userEvent.setup();
    render(<DocumentEditor document={mockDocument} />);
    
    await user.type(screen.getByRole('textbox'), '!');
    expect(screen.getByText('Hello world!')).toBeInTheDocument();
  });
});

Utility Tests (80% coverage target):

  • Test state normalization functions
  • Validate collaborative editing algorithms
  • Test document diff/merge logic
  • Verify performance optimization utilities

Integration Testing Strategy

Feature Integration Tests (50% coverage target):

  • End-to-end collaborative editing flows
  • Real-time synchronization between multiple users
  • Server state reconciliation after network interruptions
  • Document version conflict resolution

API Integration Tests:

  • Test document save/load operations with mock server
  • Validate WebSocket connection handling and reconnection logic
  • Verify proper error responses from API endpoints

E2E Testing Scope

Cypress-based E2E Tests (30% coverage target):

  • Multi-user collaborative editing scenarios
  • Network failure recovery flows
  • Document version conflict resolution workflows
  • Authentication and authorization boundary tests

Critical User Journeys Covered:

  1. User joins collaborative session
  2. Real-time content editing with others
  3. Document save and version history
  4. Network disconnect/reconnect handling
  5. User permission management

Visual Regression Testing

Implementation:

  • Use Percy or Chromatic for visual regression testing
  • Test across major browsers (Chrome, Firefox, Safari)
  • Capture key states: editor view, collaboration indicators, error states
  • Test responsive layouts on mobile/tablet breakpoints

Accessibility Testing Automation

Axe-core Integration:

// Example accessibility test
import { axe, toHaveNoViolations } from 'axe-core';
import { render } from '@testing-library/react';

expect.extend(toHaveNoViolations);

it('maintains WCAG 2.1 AA compliance', async () => {
  const { container } = render(<DocumentEditor document={mockDocument} />);
  const results = await axe.run(container);
  expect(results).toHaveNoViolations();
});

Test Data Strategy

Test Fixtures:

  • Create realistic document content with various formatting
  • Generate multiple user profiles with different permission levels
  • Mock collaborative scenarios with 2-10 simultaneous editors
  • Simulate network latency and failure conditions

Test Data Generation:

  • Use Faker.js for realistic content generation
  • Implement factory patterns for test document creation
  • Create parameterized test suites for edge cases (empty docs, large content, etc.)

Test Environment Configuration

Isolated Test Environments:

  • Use Jest with jsdom for DOM simulation
  • Implement mock WebSocket server for real-time testing
  • Configure separate test database instances for state tests
  • Set up isolated browser contexts for E2E testing

Test Data Isolation:

  • Use in-memory databases for unit tests
  • Implement test cleanup between test runs
  • Ensure no cross-contamination between test suites

9. Accessibility (A11y)

WCAG 2.1 AA compliance checklist for this feature

The collaborative document editor must meet WCAG 2.1 AA standards with specific focus on:

  • Text Contrast: All text must have a contrast ratio of at least 4.5:1 against its background (minimum 3:1 for large text)
  • Keyboard Navigation: Full keyboard accessibility for all interactive elements (tab order, focus indicators, skip links)
  • Screen Reader Support: Proper ARIA roles and semantic HTML for document content and collaborative UI
  • Focus Management: Clear focus indicators that are visible and persistent during keyboard navigation
  • Alternative Text: Descriptive alt text for any non-text content (e.g., avatars, status indicators)
  • Error Handling: Clear error messages with sufficient context for screen reader users
  • Time-based Content: No time limits that prevent users from completing tasks (unless essential)
  • Input Assistance: Clear labeling and error messages for all form fields and editing controls

Keyboard navigation support

The editor must support full keyboard navigation with the following patterns:

  1. Navigation: Tab/Shift+Tab to move between interactive elements
  2. Editing: Enter key to activate inline editing, Escape to cancel
  3. Selection: Arrow keys for text selection, Ctrl+A to select all
  4. Collaboration: Keyboard shortcuts for user presence indicators and comments (e.g., Ctrl+Shift+C for comments)
  5. Shortcuts: Consistent and documented keyboard shortcuts for common actions

Focus management strategy

Focus management will implement:

  • Focus Trapping: Within collaborative modals and comment threads to prevent focus from escaping
  • Focus Restoration: Return focus to the previous element when modals close or editing sessions end
  • Visual Indicators: Clear, high-contrast focus rings that are visible on all interactive elements
  • Focus Order: Logical tab order that follows document reading flow and semantic structure
// Focus management hook for collaborative editor components
const useFocusManagement = () => {
  const [activeElement, setActiveElement] = useState<HTMLElement | null>(null);
  
  const handleFocus = (element: HTMLElement) => {
    setActiveElement(element);
  };
  
  const restoreFocus = useCallback(() => {
    if (activeElement && document.contains(activeElement)) {
      activeElement.focus();
    }
  }, [activeElement]);
  
  return { handleFocus, restoreFocus };
};

ARIA roles and attributes plan

Key ARIA implementation:

  • Document Structure: role="document" on the main editor container
  • User Presence: aria-label with user names and status for avatars, role="status" for real-time indicators
  • Editing Context: aria-live regions for collaborative updates, aria-describedby for editing context
  • Comments: role="comment" with aria-haspopup="true" for comment threads
  • Selection: aria-selected on selected text ranges, aria-current="true" for active editing areas

Screen reader compatibility

Screen reader support will include:

  1. Semantic HTML: Use of proper heading hierarchy, lists, and form elements
  2. Dynamic Content Updates: aria-live regions for collaborative edits with polite announcement
  3. Status Updates: Real-time status messages using aria-live="polite" for user presence and editing status
  4. Keyboard Shortcuts: Clear announcements of available keyboard shortcuts using aria-describedby
  5. Error Messages: Properly announced error messages with clear context for screen reader users
// Screen reader accessible status updates component
const StatusUpdate = ({ message, type = 'polite' }: { 
  message: string; 
  type?: 'polite' | 'assertive' 
}) => {
  return (
    <div 
      aria-live={type} 
      aria-atomic="true"
      className="sr-only"
    >
      {message}
    </div>
  );
};

Keyboard navigation implementation

The editor will implement keyboard navigation patterns:

  1. Text Editing: Arrow keys for navigation, Ctrl+Shift+Arrow for word selection
  2. Collaboration: Alt+Shift+M to open comments panel, Ctrl+Shift+C for new comment
  3. User Presence: Tab navigation through user avatars and status indicators
  4. Document Navigation: Ctrl+Shift+Up/Down for section navigation, Home/End for line navigation

Focus trapping in collaborative modals

Collaborative modals will implement focus trapping:

// Focus trap implementation for comment and user presence modals
const useFocusTrap = (isOpen: boolean, modalRef: React.RefObject<HTMLDivElement>) => {
  useEffect(() => {
    if (!isOpen) return;
    
    const handleKeyDown = (e: KeyboardEvent) => {
      if (e.key === 'Tab') {
        const focusableElements = modalRef.current?.querySelectorAll(
          'button, input, textarea, select, [tabindex]:not([tabindex="-1"])'
        ) || [];
        
        const firstElement = focusableElements[0] as HTMLElement;
        const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement;
        
        if (e.shiftKey && document.activeElement === firstElement) {
          lastElement.focus();
          e.preventDefault();
        } else if (!e.shiftKey && document.activeElement === lastElement) {
          firstElement.focus();
          e.preventDefault();
        }
      }
    };
    
    document.addEventListener('keydown', handleKeyDown);
    return () => document.removeEventListener('keydown', handleKeyDown);
  }, [isOpen]);
};

Accessible color schemes and contrast

The editor will support:

  1. High Contrast Mode: Support for system high contrast themes
  2. Color Blindness: Color palette that works with various types of color blindness (deuteranopia, protanopia, tritanopia)
  3. Customizable Themes: Allow users to select accessible color themes
  4. Visual Feedback: Clear visual feedback for all interactive states (hover, focus, active)

Alternative text and semantic content

All non-text elements will have appropriate alternative text:

  • User avatars: alt="User name - Online/Offline status"
  • Editing indicators: aria-label="Current editing user - Last updated 2 minutes ago"
  • Comment threads: aria-label="Comment thread with X comments"
  • Collaborative status: aria-label="X users currently editing this document"

Error handling for screen readers

Error messages will be:

  1. Announced: Using aria-live="assertive" for critical errors
  2. Contextual: Include clear context about what went wrong and how to fix it
  3. Persistent: Errors remain visible until acknowledged by user
  4. Focus Management: Automatically focus to error messages for screen reader users

Testing approach for accessibility

Accessibility testing will include:

  1. Automated Testing: axe-core integration in CI pipeline with custom rules for collaborative features
  2. Manual Testing: Keyboard-only navigation testing and screen reader testing (NVDA, JAWS, VoiceOver)
  3. User Testing: Inclusive user testing with individuals who have disabilities
  4. Compliance Validation: Regular validation against WCAG 2.1 AA standards using automated tools and manual checks

10. Security Considerations

XSS Prevention Strategy

  • Content sanitization: All user-generated content must be sanitized using a robust HTML sanitizer (e.g., DOMPurify) before rendering to prevent script injection
  • Attribute validation: All HTML attributes must be validated and stripped of potentially dangerous values (e.g., onload, onclick)
  • Content Security Policy (CSP): Implement strict CSP headers to block inline scripts and restrict external resource loading
  • Input encoding: All user inputs are properly encoded when used in HTML context to prevent XSS
// Example content sanitization implementation
import DOMPurify from 'dompurify';

const sanitizeContent = (content: string): string => {
  return DOMPurify.sanitize(content, {
    ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'ul', 'ol', 'li'],
    ALLOWED_ATTR: ['class'],
    FORBID_TAGS: ['script', 'iframe', 'object', 'embed']
  });
};

CSRF Protection

  • CSRF tokens: Implement server-side CSRF token generation and validation for all state-modifying requests
  • Token placement: Tokens stored in HTTP-only cookies with same-site attributes for protection
  • Request validation: All write operations must include valid CSRF token in request headers or body

Content Security Policy (CSP) Headers

  • Strict CSP: Configure CSP headers with default-src 'self' and script-src 'self' 'unsafe-inline' (for development only)
  • Dynamic script restrictions: Prevent inline JavaScript execution in production
  • Resource loading control: Restrict external image, script, and stylesheet sources to known domains

Sensitive Data Handling in Frontend State

  • Token management: Auth tokens never stored in localStorage/sessionStorage; use HTTP-only cookies with secure flags
  • Data encryption: Sensitive document content is never stored in client-side state without encryption
  • Memory cleanup: Clear sensitive data from memory when no longer needed using delete operator and null assignment

Input Sanitization Approach

  • Real-time validation: Validate all input fields for malicious patterns (e.g., <script>, javascript: URLs)
  • Rate limiting: Implement rate limiting on content submission to prevent abuse
  • Content length limits: Enforce maximum document size and individual content element limits
  • Type validation: Validate all content types before processing to prevent type confusion attacks
// Input sanitization validation
const validateInput = (input: string): boolean => {
  const maliciousPatterns = [
    /<script[^>]*>.*?<\/script>/i,
    /javascript:/i,
    /on\w+\s*=\s*["'][^"']*["']/i
  ];
  
  return !maliciousPatterns.some(pattern => pattern.test(input));
};

Security Headers Implementation

// Example security headers configuration
const securityHeaders = {
  'Content-Security-Policy': "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'",
  'X-Content-Type-Options': 'nosniff',
  'X-Frame-Options': 'DENY',
  'X-XSS-Protection': '1; mode=block',
  'Strict-Transport-Security': 'max-age=31536000; includeSubDomains'
};

Authentication & Authorization

  • Role-based access control: Implement RBAC for document permissions (read, write, admin)
  • Token refresh mechanism: Secure token refresh without exposing credentials
  • Session management: Implement proper session timeout and invalidation on logout

Secure Communication

  • HTTPS enforcement: All API calls must use HTTPS only with certificate validation
  • Secure WebSocket connections: Use WSS for real-time collaboration communication
  • Certificate pinning: Implement certificate pinning for critical API endpoints

Data Protection Considerations

  • End-to-end encryption: For sensitive documents, implement client-side encryption before transmission
  • Audit logging: Log all document access and modification events for security monitoring
  • Data retention policies: Implement automatic cleanup of stale collaboration sessions

Third-party Library Security

  • Dependency scanning: Regular security scanning of all frontend dependencies using tools like Snyk or npm audit
  • Vulnerability patching: Maintain up-to-date dependencies with security patches applied promptly
  • Library verification: Verify all third-party libraries for known vulnerabilities before integration

Security Testing Integration

  • Automated security scanning: Integrate security scanners in CI/CD pipeline for XSS, CSRF, and other vulnerabilities
  • Penetration testing: Regular security assessments of the collaboration feature
  • Security code review: Include security checks in code review process for all new features

11. Internationalization (i18n)

Translation Key Structure and Naming Convention

Following BEM-style naming with feature-scoped keys:

// Translation keys organized by feature modules
{
  "document": {
    "title": "Document",
    "save": "Save",
    "saved": "Saved",
    "unsaved_changes": "You have unsaved changes"
  },
  "collaboration": {
    "online_users": "{{count}} online user",
    "online_users_plural": "{{count}} online users",
    "invite_collaborator": "Invite collaborator",
    "user_joined": "{{user}} joined the document"
  },
  "toolbar": {
    "bold": "Bold",
    "italic": "Italic",
    "underline": "Underline",
    "bullet_list": "Bullet List"
  },
  "modals": {
    "invite_user": "Invite User",
    "confirm_leave": "Confirm Leave"
  }
}

RTL Support Considerations

RTL layout support will be implemented through CSS classes and direction properties:

/* CSS variables for text direction */
:root {
  --text-direction: ltr;
}

[dir="rtl"] {
  --text-direction: rtl;
}

/* Component-level RTL handling */
.editor-container {
  direction: var(--text-direction);
  text-align: var(--text-direction, left);
}

Date, Number, Currency Formatting Strategy

Using Intl API for locale-aware formatting:

// i18n utility functions
const formatDate = (date: Date, locale: string): string => {
  return new Intl.DateTimeFormat(locale, {
    year: 'numeric',
    month: 'short',
    day: 'numeric',
    hour: '2-digit',
    minute: '2-digit'
  }).format(date);
};

const formatNumber = (value: number, locale: string): string => {
  return new Intl.NumberFormat(locale, {
    maximumFractionDigits: 2
  }).format(value);
};

Locale Detection and Switching Strategy

Automatic detection using browser navigator.language, with manual override:

// i18n context implementation
interface I18nContextType {
  locale: string;
  setLocale: (locale: string) => void;
  t: (key: string, params?: Record<string, any>) => string;
  formatDate: (date: Date) => string;
}

const I18nContext = React.createContext<I18nContextType>({
  locale: 'en-US',
  setLocale: () => {},
  t: () => '',
  formatDate: () => ''
});

// Auto-detect based on browser language
const detectLocale = (): string => {
  const browserLang = navigator.language;
  const supportedLocales = ['en-US', 'fr-FR', 'es-ES', 'ar-SA'];
  
  // Match exact or fallback to default
  return supportedLocales.find(locale => 
    locale === browserLang || 
    locale.startsWith(browserLang.split('-')[0])
  ) || 'en-US';
};

Translation File Organization

{
  "en-US": {
    "document": {
      "title": "Document",
      "save": "Save",
      "saved": "Saved"
    },
    "collaboration": {
      "online_users": "{{count}} online user",
      "invite_collaborator": "Invite collaborator"
    }
  },
  "fr-FR": {
    "document": {
      "title": "Document",
      "save": "Sauvegarder",
      "saved": "Sauvegardé"
    },
    "collaboration": {
      "online_users": "{{count}} utilisateur en ligne",
      "invite_collaborator": "Inviter un collaborateur"
    }
  },
  "ar-SA": {
    "document": {
      "title": "مستند",
      "save": "حفظ",
      "saved": "تم الحفظ"
    },
    "collaboration": {
      "online_users": "{{count}} مستخدم متصل",
      "invite_collaborator": "دعوة متعاون"
    }
  }
}

12. Monitoring & Observability

Real User Monitoring (RUM) Metrics to Track

The collaborative document editor requires comprehensive RUM metrics to ensure optimal user experience and detect performance regressions:

  • Document Load Time: Time from initial route load to fully rendered document (target: <2s for 90% of users)
  • Edit Latency: Time from user input to visible change in document (target: <100ms for 95% of edits)
  • Sync Latency: Time from edit to synchronization with other collaborators (target: <500ms for 90% of syncs)
  • First Contentful Paint (FCP): Time to first content rendering (target: <1.8s)
  • Largest Contentful Paint (LCP): Time to render main content (target: <2.5s)
  • Cumulative Layout Shift (CLS): Layout stability during document editing (target: <0.1)
  • Interaction to Next Paint (INP): Responsiveness of user interactions (target: <200ms)

Error Tracking Integration

Sentry Integration Strategy:

  • Capture all unhandled promise rejections and synchronous errors
  • Track client-side errors with user context (document ID, collaborator ID)
  • Implement error grouping by error type and severity level
  • Set up alerting for critical errors (5xx errors, sync failures, authentication failures)

Custom Events and User Journey Tracking

Key User Journey Events:

// Event tracking interface
interface UserEvent {
  event: string;
  timestamp: number;
  userId?: string;
  documentId?: string;
  properties?: Record<string, any>;
}

// Example event tracking
const trackUserEvent = (event: UserEvent) => {
  // Send to analytics backend or RUM service
  console.log('Tracking event:', event);
};

Document Editing Journeys:

  • document.opened - when document is first loaded
  • document.saved - when document is saved (local or remote)
  • edit.applied - when user makes an edit with timestamp and operation type
  • sync.completed - when sync completes successfully or fails
  • collaborator.joined - when new collaborator joins document
  • collaborator.left - when collaborator leaves document

Performance Monitoring and Regression Detection

Performance Baseline Strategy:

  • Establish performance baselines for different document sizes (small, medium, large)
  • Set up automated regression detection for Core Web Vitals
  • Implement performance budgets with build-time checks (webpack bundle analyzer, Lighthouse CI)

Monitoring Dashboard Requirements:

  • Real-time document performance dashboard
  • Historical trend analysis for key metrics
  • Alert thresholds with configurable severity levels
  • Collaborator count impact analysis on performance

Sample Performance Monitoring Hook:

// Performance monitoring hook for document editor
const useDocumentPerformance = (documentId: string) => {
  const [metrics, setMetrics] = useState<DocumentMetrics>({
    loadTime: 0,
    editLatency: [],
    syncLatency: [],
    fps: 0
  });

  useEffect(() => {
    // Setup performance monitoring
    const startLoad = performance.now();
    
    // Track edit latency
    const editObserver = new PerformanceObserver((list) => {
      list.getEntries().forEach((entry) => {
        if (entry.entryType === 'measure') {
          // Track specific document operations
          setMetrics(prev => ({
            ...prev,
            editLatency: [...prev.editLatency, entry.duration]
          }));
        }
      });
    });

    editObserver.observe({ entryTypes: ['measure'] });

    return () => {
      editObserver.disconnect();
    };
  }, [documentId]);

  return metrics;
};

interface DocumentMetrics {
  loadTime: number;
  editLatency: number[];
  syncLatency: number[];
  fps: number;
}

Alerting Strategy:

  • Critical alerts for sync failures (>5% failure rate)
  • Warning alerts for performance degradation (10% increase in edit latency)
  • Info alerts for new user behaviors or feature adoption patterns

Data Retention and Privacy:

  • All monitoring data is anonymized with user IDs hashed for correlation
  • GDPR-compliant data collection and retention policies (30 days for performance data)
  • User opt-out capability for detailed performance tracking

Integration with Existing Monitoring Stack:

  • Send metrics to existing infrastructure (Prometheus/Grafana)
  • Support for custom metric ingestion endpoints
  • Integration with CI/CD pipelines for performance regression testing

13. File & Folder Structure

Proposed Directory Structure

/src
  /features
    /collaborative-editor
      /components
        EditorContainer.tsx
        Toolbar.tsx
        DocumentView.tsx
        UserList.tsx
        CollaborationIndicator.tsx
      /hooks
        useEditorState.ts
        useCollaboration.ts
        useDocumentSync.ts
      /services
        editor-api.ts
        websocket-client.ts
      /types
        index.ts
        document.types.ts
        user.types.ts
      /utils
        editor-utils.ts
        collaboration-utils.ts
      index.ts
  /shared
    /components
      Button.tsx
      Input.tsx
      Modal.tsx
      Spinner.tsx
    /hooks
      useDebounce.ts
      useThrottle.ts
    /utils
      dom-utils.ts
      validation-utils.ts
    /types
      common.types.ts
  /stores
    editor-store.ts
    user-store.ts
  /routes
    editor.routes.tsx
  /services
    api-client.ts
    auth-service.ts
  /types
    global.types.ts

Module Boundaries and Import Rules

  • Feature isolation: All collaborative editor functionality lives under /features/collaborative-editor with clear internal exports
  • No cross-feature imports: Features cannot directly import from other features (e.g., /features/collaborative-editor cannot import from /features/comments)
  • Shared components: Reusable UI elements live in /shared/components and are imported by features as needed
  • Barrel exports: Each feature has an index.ts that re-exports the most commonly used components and hooks for clean imports

Shared vs Feature-Specific Code Separation

Shared code (reusable across features):

  • Generic UI components (Button, Input, Modal)
  • Common utility functions (useDebounce, useThrottle)
  • Global type definitions
  • Common services (API client, auth service)

Feature-specific code:

  • Editor-specific components and hooks
  • Document state management logic
  • Collaboration protocol implementations
  • Feature-level utilities and services

This structure enables:

  1. Clear ownership boundaries between features
  2. Easy code reuse through shared components
  3. Isolated development and testing of feature modules
  4. Scalable organization as new features are added

14. Dependencies & Third-Party Libraries

Key Libraries Selected with Justification

1. React + React DOM

  • Justification: Core framework for building the UI. React's component model and hooks provide the foundation for state management and reactivity.
  • Version: ^18.2.0
  • Bundle size impact: Minimal, as it's already a core dependency

2. TypeScript

  • Justification: Essential for type safety in large-scale applications, catching runtime errors at compile time and enabling better IDE support.
  • Version: ^5.0.0
  • Bundle size impact: None (compile-time only)

3. Tailwind CSS + PostCSS

  • Justification: Utility-first CSS framework that enables rapid UI development with consistent design system. PostCSS provides necessary transformations for modern CSS features.
  • Version: Tailwind 3.3+, PostCSS 8.4+
  • Bundle size impact: Moderate (tailwind base + components), but highly optimized via purge

4. React Quill / Draft.js / TipTap

  • Justification: Rich text editing capabilities with collaborative features. TipTap chosen for its modular architecture, extensibility, and active community.
  • Version: TipTap 2.x
  • Bundle size impact: Moderate (core + extensions)

5. Socket.IO Client

  • Justification: Real-time communication layer for collaborative editing features (live cursors, real-time updates). Provides automatic reconnection and message buffering.
  • Version: ^4.7.2
  • Bundle size impact: Moderate (websocket + transport layer)

6. Zustand / Jotai / React Query

  • Justification: State management solution for collaborative editing. Zustand selected due to its simplicity, small bundle size, and ability to handle both local and shared state.
  • Version: Zustand 4.x
  • Bundle size impact: Minimal (1KB)

7. React Virtualized / react-window

  • Justification: Efficient rendering of large document content with virtualization to maintain performance.
  • Version: react-window 1.8+
  • Bundle size impact: Minimal (virtualization logic only)

8. Axios / Fetch Wrapper

  • Justification: HTTP client for API communication with interceptors and request/response handling.
  • Version: Axios 1.4+
  • Bundle size impact: Moderate (request/response handling layer)

9. date-fns / dayjs

  • Justification: Date/time formatting and manipulation with minimal bundle size and internationalization support.
  • Version: dayjs 1.11+
  • Bundle size impact: Minimal (only required modules)

Alternatives Considered and Rejected

1. Draft.js vs TipTap

  • Rejected: Draft.js has a complex API and is harder to customize for collaborative features. TipTap offers better extensibility with a more modern architecture.

2. Redux Toolkit vs Zustand

  • Rejected: While Redux Toolkit provides mature tooling, Zustand's simplicity and smaller bundle size make it more suitable for this feature's scope.

3. Material UI vs Tailwind CSS

  • Rejected: Material UI provides extensive components but introduces significant bundle bloat. Tailwind CSS offers more control with better performance characteristics.

4. React Query vs Custom Fetch Implementation

  • Rejected: While custom implementation is possible, React Query provides robust caching, automatic refetching, and error handling that's crucial for collaborative document editing.

Licensing and Bundle Size Impact Assessment

Bundle Size Budget Allocation:

  • Core Framework: ~20KB (React + DOM)
  • UI Components: ~150KB (Tailwind + TipTap)
  • State Management: ~2KB (Zustand)
  • Real-time Communication: ~40KB (Socket.IO)
  • HTTP Client: ~25KB (Axios)
  • Performance Utilities: ~10KB (react-window)

Total Estimated Bundle Size: ~250KB for core features, with optional extensions up to 400KB.

Licensing Compliance:

  • All libraries are MIT or BSD licensed, compatible with commercial use
  • No proprietary dependencies that would restrict deployment or distribution
  • Open source ecosystem allows for customization and auditing

Tree-shaking Strategy:

  • All libraries support tree-shaking to minimize bundle size
  • Only required modules are imported (e.g., specific date-fns functions instead of entire library)
  • React Query and Zustand support selective imports for minimal footprint

15. Open Questions, Risks & Trade-offs

Decisions that need product/engineering alignment

Real-time synchronization strategy: The system must decide between operational transformation (OT) vs. conflict-free replicated data types (CRDTs). OT offers more predictable merging but is complex to implement and maintain. CRDTs provide simpler conflict resolution but may not handle all edge cases in collaborative editing scenarios.

Versioning and history management: Product decision required on whether to store full document history or only recent changes. Full history provides better audit capabilities but increases storage and processing overhead significantly.

Offline support granularity: The system must decide whether to support offline editing with eventual consistency or require online connectivity for all operations. This impacts the complexity of local state management and synchronization logic.

Known trade-offs made in this design

Simplified conflict resolution: The design uses a basic last-write-wins strategy for content conflicts rather than implementing sophisticated OT or CRDT-based conflict resolution. This trade-off simplifies implementation but may lead to data loss in high-conflict scenarios.

Client-side state management: Using React Context and local state for collaborative features instead of centralized state management (Redux/Zustand) reduces complexity but may not scale well for very large collaborative documents or complex state interactions.

Simplified user presence system: User avatars and real-time indicators are implemented with basic polling rather than WebSockets for user presence tracking. This reduces infrastructure costs but introduces latency in presence updates.

Limited formatting support: The design focuses on basic text formatting (bold, italic) rather than comprehensive rich-text formatting to reduce complexity and performance overhead in the collaborative editing experience.

Risks and mitigations

Network partition risks: In distributed systems, network partitions can cause divergent document states. Mitigation includes implementing proper conflict resolution strategies and maintaining local document state with proper version tracking.

Performance degradation under high concurrent users: Risk of UI lag or memory issues when many users edit simultaneously. Mitigation includes implementing proper virtualization for large documents, efficient diff algorithms, and client-side state normalization.

Security vulnerabilities in real-time communication: Risk of unauthorized access to collaborative documents. Mitigation includes robust authentication, authorization checks, and proper WebSocket security configurations.

Data consistency issues: Risk of data loss or corruption during synchronization. Mitigation includes proper optimistic updates, server-side validation, and comprehensive error handling with user feedback.

Technical debt acknowledged

Monolithic document state management: The current design treats the entire document as a single state object, which will become unwieldy as document complexity increases. Future refactoring should consider breaking down the document into smaller, manageable chunks with proper normalization.

Limited extensibility: The current architecture makes it difficult to add new collaborative features (e.g., comments, mentions) without significant refactoring. The component architecture should be designed with extensibility in mind from the beginning.

Testing complexity: The collaborative nature of the feature introduces significant testing complexity, particularly for race conditions and synchronization edge cases. More comprehensive integration and E2E tests will be required to ensure correctness.

Performance monitoring gaps: While performance targets are defined, the system lacks comprehensive real-time performance monitoring to detect degradation in collaborative editing experiences.

Was this article helpful?

Topics in this article

More from Frontend LLD

  • Backend HLD

    Collaborative Document Editor System Design

    Comprehensive backend architecture for real-time collaborative document editing with conflict resolution and synchronization mechanisms.

  • Frontend HLD

    Offline Data Conflict Resolution System Design

    Comprehensive frontend architecture for handling data conflicts when users work offline and later synchronize with backend systems.

  • Frontend HLD

    Real-Time State Synchronization System Design

    Comprehensive frontend architecture for building scalable real-time state synchronization across distributed client applications with conflict resolution and offline support.

  • Frontend LLD

    Excel-Style Spreadsheet UI Design and Implementation

    Comprehensive low-level design for building a spreadsheet interface with cell editing, formulas, and real-time collaboration capabilities.

Stay in the Loop

Get curated DSA patterns, coding problems, frontend and backend LLD/HLD, and AI/ML System Design insights delivered straight to your inbox. No spam, just focused interview and engineering content.