Quick Answer

Real-time collaborative editors like Google Docs and Figma use one of two approaches to keep every user's copy consistent. Operational Transformation (OT) transforms incoming edits against a central server's version history before applying them. CRDTs (Conflict-free Replicated Data Types) give every edit a unique, comparable ID so replicas can merge independently, in any order, and still end up identical. OT needs a central authority to sequence edits; CRDTs don't, which is why offline-first and peer-to-peer tools tend to reach for them.

The Problem: Two People, One Document, Same Instant

Say a shared document currently reads “AC”. Two people both put their cursor between A and C at the same moment — network latency means neither has seen the other's keystroke yet. User 1 types “B”. User 2 types “X”. Now there are two different documents loose in the system: “ABC” on one machine, “AXC” on another.

Every real-time collaborative editor has to resolve this without asking a human, deterministically, and the resolution has to land on the exact same final document on every client — even if the network happens to deliver these two edits to different clients in a different order. Any correct approach needs two properties: convergence (everyone ends up with the identical final state) and intention preservation (nobody's edit gets silently dropped or scrambled beyond recognition). The two dominant techniques, Operational Transformation and CRDTs, get to convergence through completely different mechanics.

Operational Transformation: Transform Before You Apply

OT represents every edit as an operation with a position — insert("B", 1), delete(3) — rather than sending the whole document around. When a server (or a peer) receives an operation generated against an older version of the document than the one it currently holds, it doesn't apply the operation as-is. It transforms the operation's position first, to account for what has already changed underneath it.

Back to the AC example: User 2's operation was insert("X", 1), generated when the document was still “AC”. By the time the server has already applied User 1's insert("B", 1) and the document is “ABC”, blindly inserting “X” at raw position 1 would land it between A and B, splitting User 1's edit apart. OT's transform function shifts User 2's operation to insert("X", 2) instead, so it lands after “AB” — preserving where User 2 actually meant to type relative to the surrounding text, giving “ABXC”.

This is genuinely how Google Docs works: clients send operations, not document snapshots, and a central server assigns each one a sequence number and transforms concurrent operations against each other before broadcasting the agreed order back out to everyone. The catch is that correctly transforming every pair of operation types against each other — insert against insert, insert against delete, delete against delete, formatting operations against text operations — is notoriously easy to get subtly wrong. Production-grade OT took Google years to fully harden. And OT fundamentally wants a central server to arbitrate order, which makes true offline or peer-to-peer editing awkward.

CRDTs: Make Merging Commutative Instead

CRDTs solve the same problem from a different direction. Instead of transforming operations relative to each other after the fact, give every inserted element a globally unique, comparable ID up front — typically a counter local to the replica plus that replica's own ID — along with a pointer to the element it was inserted after. Because that ID never changes and is comparable using the exact same rule on every replica, any replica can slot a remote insert into its local copy correctly, regardless of what order operations arrive in, without asking a server what the “real” order is.

This is the idea behind RGA (Replicated Growable Array), one of the classic sequence CRDTs, and it's the same core principle — with more machinery for other data shapes — behind production libraries like Yjs and Automerge.

Building and Running a Minimal CRDT

Rather than just asserting that CRDTs converge, I implemented a simplified RGA in Python and ran the exact AC/B/X scenario above through it:

class RGA:
    def __init__(self, replica_id):
        self.replica_id = replica_id
        self.clock = 0
        self.elements = []   # ordered list of {id, value, after, tombstone}
        self.pending = {}    # after_id -> ops waiting on that parent to arrive

    def _next_id(self):
        self.clock += 1
        return (self.clock, self.replica_id)

    def local_insert(self, index, value):
        visible = [e for e in self.elements if not e['tombstone']]
        after_id = visible[index - 1]['id'] if index > 0 else None
        new_id = self._next_id()
        op = {'id': new_id, 'value': value, 'after': after_id, 'tombstone': False}
        self._apply(op)
        return op

    def _has(self, op_id):
        return any(e['id'] == op_id for e in self.elements)

    def _insert_ordered(self, op):
        if op['after'] is None:
            pos = 0
            while (pos < len(self.elements)
                   and self.elements[pos]['after'] is None
                   and self.elements[pos]['id'] > op['id']):
                pos += 1
            self.elements.insert(pos, op)
        else:
            anchor_pos = next(i for i, e in enumerate(self.elements) if e['id'] == op['after'])
            pos = anchor_pos + 1
            while (pos < len(self.elements)
                   and self.elements[pos]['after'] == op['after']
                   and self.elements[pos]['id'] > op['id']):
                pos += 1
            self.elements.insert(pos, op)

    def _apply(self, op):
        if self._has(op['id']):
            return  # idempotent: already applied
        if op['after'] is not None and not self._has(op['after']):
            self.pending.setdefault(op['after'], []).append(op)
            return
        self._insert_ordered(op)
        self.clock = max(self.clock, op['id'][0])
        for waiter in self.pending.pop(op['id'], []):
            self._apply(waiter)

    def merge_op(self, op):
        self._apply(op)

    def text(self):
        return ''.join(e['value'] for e in self.elements if not e['tombstone'])

The first version of this script actually failed. When I fed a third replica the same four operations in a deliberately scrambled order, it crashed trying to place an insert whose “parent” element hadn't arrived at that replica yet. That's not a toy bug — it's a real, well-documented property of operation-based CRDTs: an insert's position is only well-defined once the element it was inserted after already exists locally. Any correct implementation, RGA included, has to buffer an operation until its dependency shows up and replay it once it does. That's exactly what Yjs and Automerge do internally. I added that buffering (the pending dictionary above) and reran it. Here's the real, corrected output:

After initial sync -- A: AC | B: AC

Before merge:
  A's local doc: ABC
  B's local doc: AXC

After merge (A applied B's op, B applied A's op):
  A's doc: AXBC
  B's doc: AXBC
  Converged: True

Replica C applied ops in a different order: [(2,'A'), (3,'B'), (1,'A'), (3,'A')]
  C's doc: AXBC
  Matches A and B: True

Replica D applied ops in yet another order.
  D's doc: AXBC
  Matches everyone: True

Final converged document across all 4 replicas: 'AXBC' 

All four replicas — A and B merging each other's concurrent op, plus C and D receiving the same four operations in two more different orders — landed on the identical string “AXBC”. That's the CRDT convergence guarantee, demonstrated rather than asserted.

OT vs CRDTs: What Each Actually Costs You

OT needs a central server as the single source of truth for operation ordering. That's a real coordination cost, but it keeps per-client metadata small — you don't need to keep every character's unique ID around forever.

CRDTs need no central authority, which is exactly what makes true peer-to-peer and offline-first editing work. The cost shows up elsewhere: every element permanently carries its unique ID, and deletions typically leave a tombstone behind rather than truly disappearing, since a tombstone might still be the “after” anchor some other in-flight operation refers to. A heavily-edited CRDT document can accumulate metadata that never shrinks on its own, unless the system runs a separate garbage-collection pass once every replica has confirmed it has seen a given deletion.

The real-world split is more nuanced than “Google Docs = OT, everyone else = CRDT.” Google Docs does use OT with a central server. Figma is frequently described as CRDT-based, but Figma's own engineering blog is precise that it isn't a textbook OT system and isn't a pure CRDT either — it's a server-authoritative model inspired by CRDT last-writer-wins registers, where the server still decides the final order for two clients writing to the same property on the same object, deliberately avoiding vector clocks and tombstone garbage collection. That design fits Figma's tree-of-objects document model better than a text-sequence CRDT like RGA would.

Choosing Between Them in a Real Product

Reach for OT when your product already assumes a persistent connection to a real-time server and you're dealing with complex character-level text semantics — it's a mature, well-trodden path, and the hard part (correct transform functions) is already solved in battle-tested implementations you don't have to rebuild.

Reach for a CRDT when offline editing is a real requirement (edit on a flight, sync when you land), or when you want genuine peer-to-peer sync with no central server at all. CRDTs also tend to fit non-text data shapes more naturally — Figma's object trees, shared counters, shared sets — where a sequence-position transform doesn't apply in the first place.

And as Figma's own architecture shows, plenty of production systems land somewhere in between rather than picking one option straight out of a textbook.

Frequently Asked Questions

Is Google Docs built on CRDTs? No. Google Docs uses Operational Transformation with a central server that sequences and transforms concurrent operations; OT predates CRDTs as a well-known technique and remains Google Docs' core model.
Does Figma use CRDTs? Not a textbook CRDT. Figma's own engineering blog describes a server-authoritative model inspired by CRDT last-writer-wins registers, where the server still resolves the final order for conflicting edits to the same object property.
What does it actually mean for a CRDT to 'converge'? It means that regardless of what order two replicas receive the same set of concurrent operations in, they end up with byte-for-byte the same final state. I proved this by feeding four replicas the same four operations in four different orders and getting an identical document every time.
Why can't a CRDT just apply operations in the order they arrive? Because an insert's position often depends on another element already being present locally (the one it was inserted 'after'). If that parent hasn't arrived yet, a correct implementation has to buffer the operation and replay it once the dependency shows up — skipping this is exactly the crash I hit while building the example.
Which is better for offline editing, OT or CRDTs? CRDTs, generally. OT's transform step assumes you can reach a central server (or at least the peer whose operations you're transforming against) to agree on ordering; CRDTs let two devices edit independently for days and merge correctly whenever they eventually reconnect.