When our client approached us to build "Figma-like" collaboration into their existing design tool, we knew we were in for a serious technical challenge. The existing application was a single-user desktop experience with a codebase that wasn't originally architected for real-time collaboration. What followed was a journey of discovery, optimization, and architectural transformation that completely changed how we think about collaborative software.
I'm sharing our experience so other development teams can benefit from the hard-won lessons we learned along the way.
Real-time collaboration isn't just about sending updates back and forth. It's about creating a consistent experience for all users when they're simultaneously modifying a complex document structure, often with unreliable network connections and varying latencies.
Our key challenges included:
After evaluating several approaches, we settled on Conflict-free Replicated Data Types (CRDTs) as our foundational technology. Unlike Operational Transformation (OT), which is used in tools like Google Docs, CRDTs don't require a central authority to resolve conflicts, making them more resilient to network issues.
Here's a simplified implementation of our CRDT-based collaborative system:
class CollaborativeDesignSystem {
  constructor(documentId, userId) {
    this.documentId = documentId;
    this.userId = userId;
    this.clock = 0;
    this.operations = [];
    this.documentState = new DocumentState();
    this.pendingOperations = new Map();
    this.network = new NetworkAdapter(documentId, userId);
    // Set up network handlers
    this.network.onOperationReceived(this._handleRemoteOperation.bind(this));
    // Set up automatic state synchronization
    this.network.onConnectionStateChanged(this._handleConnectionChange.bind(this));
  }
  // Apply a local operation to the document
  applyLocalOperation(operation) {
    // Assign a logical timestamp (Lamport clock)
    this.clock++;
    const timestamp = this.clock;
    // Create a uniquely identifiable operation
    const taggedOperation = {
      ...operation,
      id: `${this.userId}:${timestamp}`,
      userId: this.userId,
      timestamp
    };
    // Apply operation locally immediately for responsiveness
    this._applyOperation(taggedOperation);
    // Send to other collaborators
    this.network.broadcastOperation(taggedOperation);
    return taggedOperation.id;
  }
  // Handle incoming remote operations
  _handleRemoteOperation(remoteOperation) {
    // Update our logical clock to maintain causal ordering
    this.clock = Math.max(this.clock, remoteOperation.timestamp) + 1;
    // Apply the remote operation to our local state
    this._applyOperation(remoteOperation);
  }
  // Apply an operation (either local or remote) to the document state
  _applyOperation(operation) {
    // Skip if we've already applied this operation
    if (this.operations.some(op => op.id === operation.id)) {
      return;
    }
    // Check if this operation depends on operations we haven't received yet
    if (operation.dependencies) {
      const missingDependencies = operation.dependencies.filter(depId =>
        !this.operations.some(op => op.id === depId)
      );
      if (missingDependencies.length > 0) {
        // Store for later application once dependencies arrive
        this.pendingOperations.set(operation.id, operation);
        // Request missing operations from the network
        this.network.requestOperations(missingDependencies);
        return;
      }
    }
    // Process by operation type
    switch (operation.type) {
      case 'insert':
        this._handleInsertOperation(operation);
        break;
      case 'update':
        this._handleUpdateOperation(operation);
        break;
      case 'delete':
        this._handleDeleteOperation(operation);
        break;
      case 'group':
        this._handleGroupOperation(operation);
        break;
      case 'position':
        this._handlePositionOperation(operation);
        break;
      // Additional operation types...
    }
    // Record that we've applied this operation
    this.operations.push(operation);
    // Check if any pending operations can now be applied
    this._processPendingOperations();
  }
  // Process any pending operations whose dependencies are now met
  _processPendingOperations() {
    let appliedAny = false;
    this.pendingOperations.forEach((operation, id) => {
      const missingDependencies = operation.dependencies.filter(depId =>
        !this.operations.some(op => op.id === depId)
      );
      if (missingDependencies.length === 0) {
        this.pendingOperations.delete(id);
        this._applyOperation(operation);
        appliedAny = true;
      }
    });
    // Recursively process until no more can be applied
    if (appliedAny && this.pendingOperations.size > 0) {
      this._processPendingOperations();
    }
  }
  // Handle specific operation types
  _handleInsertOperation(operation) {
    const { elementType, properties, parentId, position } = operation.data;
    // Create element with a globally unique ID
    const elementId = operation.id; // Using the operation ID as the element ID
    // Add to document state
    this.documentState.createElement(elementId, elementType, properties);
    this.documentState.addToParent(elementId, parentId, position);
    // Notify UI of changes
    this._notifyUIChanges([{
      type: 'elementAdded',
      elementId,
      parentId,
      position
    }]);
  }
  _handleUpdateOperation(operation) {
    const { elementId, properties, propertyPath } = operation.data;
    // Apply property updates
    if (propertyPath) {
      // For deep property updates
      this.documentState.updateElementNestedProperty(elementId, propertyPath, properties);
    } else {
      // For shallow property updates
      this.documentState.updateElementProperties(elementId, properties);
    }
    // Notify UI of changes
    this._notifyUIChanges([{
      type: 'elementUpdated',
      elementId,
      properties,
      propertyPath
    }]);
  }
  _handleDeleteOperation(operation) {
    const { elementId } = operation.data;
    // Logical deletion (mark as deleted rather than removing)
    this.documentState.markAsDeleted(elementId);
    // Notify UI of changes
    this._notifyUIChanges([{
      type: 'elementDeleted',
      elementId
    }]);
  }
  _handlePositionOperation(operation) {
    const { elementId, x, y, strategy } = operation.data;
    // Apply position - This is where CRDT position logic happens
    if (strategy === 'absolute') {
      // Simple positioning
      this.documentState.setElementPosition(elementId, x, y);
    } else if (strategy === 'fractional') {
      // Fractional positioning (more resilient to concurrent edits)
      this.documentState.setElementFractionalPosition(elementId, x, y);
    }
    // Notify UI of changes
    this._notifyUIChanges([{
      type: 'elementMoved',
      elementId,
      position: { x, y }
    }]);
  }
  // Utility methods and connection handling...
}
One of the trickiest parts of collaborative design is handling element positioning. If two users simultaneously move the same element, how do you resolve that conflict? We solved this with a fractional positioning system inspired by the CRDT approach:
class FractionalPositioning {
  // Generate a position between two other positions
  static generatePositionBetween(before, after) {
    // If no positions exist yet
    if (!before && !after) {
      return [0.5];
    }
    // If inserting at the beginning
    if (!before) {
      // Position before the first element
      const firstDigit = after[0];
      if (firstDigit > 0) {
        return [firstDigit / 2];
      } else {
        return [0, after[1] ? after[1] / 2 : 0.5];
      }
    }
    // If inserting at the end
    if (!after) {
      // Position after the last element
      return [before[0] + 1];
    }
    // Find a position between before and after
    const beforeArr = Array.isArray(before) ? before : [before];
    const afterArr = Array.isArray(after) ? after : [after];
    // Try to insert between them
    return this._positionBetween(beforeArr, afterArr);
  }
  // Core algorithm to find a position between two others
  static _positionBetween(before, after) {
    // Compare elements at each level of the arrays
    for (let i = 0; i < Math.max(before.length, after.length); i++) {
      const beforeVal = i < before.length ? before[i] : 0;
      const afterVal = i < after.length ? after[i] : 0;
      if (beforeVal !== afterVal) {
        // Found a level where they differ, insert between these values
        const midValue = (beforeVal + afterVal) / 2;
        if (midValue !== beforeVal && midValue !== afterVal) {
          // Simple case: average is between the values
          const result = before.slice(0, i);
          result.push(midValue);
          return result;
        } else {
          // Precision limit reached, need to add another level
          const result = before.slice(0, i + 1);
          result.push(0.5);
          return result;
        }
      }
    }
    // If we get here, the arrays are identical
    // Add another level
    const result = before.slice();
    result.push(0.5);
    return result;
  }
  // Convert a fractional position to a decimal for UI rendering
  static toDecimal(fractionalPosition) {
    let result = 0;
    let divisor = 1;
    for (const value of fractionalPosition) {
      result += value / divisor;
      divisor *= 10;
    }
    return result;
  }
}
This approach allowed us to generate unique positions between existing elements that would be consistently ordered regardless of which user created them, avoiding the need for conflict resolution entirely.