Skip to main content

Command Palette

Search for a command to run...

How Collaborative Docs Work: An Introduction to CRDTs

Updated
5 min readView as Markdown
How Collaborative Docs Work: An Introduction to CRDTs

TL;DR: Collaborative documents avoid synchronization conflicts by using Conflict-free Replicated Data Types (CRDTs) instead of index-based positioning. By assigning every character a unique, immutable identifier and referencing edits to those IDs rather than numeric array indexes, concurrent changes merge predictably regardless of the order they arrive at the server.

If you have ever worked on a shared document in Google Docs or Notion, you have probably taken real-time collaboration for granted. But under the hood, syncing text across multiple distributed clients is a notoriously difficult problem. I want to dive into why naive solutions break, and explain how I think about resolving this conflict using an elegant data structure called a CRDT.

Why does index-based positioning fail in collaborative editing?

Index-based positioning fails because concurrent edits shift the character offsets of the document in real time. When multiple users send edits based on their local view of the document, the absolute indexes become desynchronized, leading to corrupt text when those edits are merged on other clients.

Imagine you are building a collaborative text editor. The document currently contains the word cat. Two users try to edit it simultaneously:

  • User 1 wants to change it to chat by adding an h at index 1.
  • User 2 wants to change it to cats by adding an s at index 3.

If you naively apply these edits using array indexes, the final state depends entirely on which edit runs first.

If User 1's edit runs first, the word becomes chat. When you then apply User 2's edit (insert s at index 3), you end up with chast—which is completely wrong. Conversely, if User 2's edit runs first, you get cats, and applying User 1's edit results in chats.

Because the order of network packets dictates the final text, clients will end up with mismatched documents.

How do CRDTs solve the collaborative text sync problem?

Conflict-free Replicated Data Types (CRDTs) solve this by assigning a globally unique, immutable identifier to every single character in a document. Edits are then declared relative to these static identifiers rather than volatile numeric indexes, ensuring that operations can be applied in any order with the same final state.

To solve this, I prefer to model the document not as a raw array of characters, but as a collection of metadata nodes. Every character node is assigned a unique ID consisting of the user's ID and a local counter.

For example, I would structure the word cat like this:

  • c -> ID: user0v1
  • a -> ID: user0v2
  • t -> ID: user0v3

When User 1 wants to insert h, they do not say "insert at index 1." Instead, they say "insert h (ID: user1v1) directly after user0v1." At the same time, User 2 inserts s (ID: user2v1) directly after user0v3.

Here is how I represent that metadata under the hood for a single edit:

{
  "id": "user1v1",
  "char": "h",
  "after": "user0v1"
}

This representation maps out how every character relates to its neighbors:

Character Unique ID Inserted After ID Final Position
c user0v1 Start 1st
h user1v1 user0v1 2nd
a user0v2 user0v1 3rd
t user0v3 user0v2 4th
s user2v1 user0v3 5th

Why is the order of applied edits irrelevant in a CRDT?

CRDTs are mathematically designed to be commutative and associative, meaning the order in which network packets arrive does not affect the final computed state. Because each insertion or deletion operation refers to static character IDs, the resulting document structure converges to the exact same state on all clients.

No matter what order the network delivers the packets, the relationship remains the same. Whether the local client processes the h insertion or the s insertion first, h will always sit after c (user0v1), and s will always sit after t (user0v3). I find this mathematical certainty incredibly satisfying because it guarantees that every client eventually converges on the exact same word: chats.

FAQ

How do CRDTs handle character deletions?

To delete a character, CRDTs typically use "tombstones." Instead of completely removing the node from the tree (which would break other pending edits referencing its ID), the node is marked as invisible. This preserves the document's relational structure while hiding the character from the user.

What is the difference between Operational Transformation (OT) and CRDTs?

Operational Transformation (OT) relies on a central server to rewrite the indexes of incoming edits before broadcasting them to other clients. CRDTs are peer-to-peer friendly; they do not require a central coordinating server to resolve conflicts because the data structures resolve themselves mathematically.

Do CRDTs make document file sizes too bloated?

Yes, CRDTs introduce metadata overhead because every single character requires an ID and positioning pointers. However, modern CRDT implementations use optimizations like run-length encoding and state-tree pruning to keep memory usage highly performant.