SevyDevy

Guide · System design

Design Google Docs: OT vs CRDTs

20 min readUpdated By Saket Bhatnagar

Researched and drafted with AI, reviewed and edited by Saket Bhatnagar.

In short

  • Collaborative editing lets several people change one document at the same time without losing anyone's text. Each edit travels as a small operation, such as insert 'Rs 40,000' at position 8, and every copy merges them by fixed rules.
  • Operational transformation (OT) rewrites each incoming edit so its position fits your copy. In the design Google described for Docs in 2010, a central server puts every edit in one numbered order.
  • A CRDT (conflict-free replicated data type) gives every character a permanent ID and places new text after an ID, not at a number. Copies that receive the same edits, in any order, end up identical without a server deciding.
  • In OT the document carries no data per character. But OT needs a correct transform rule for every pair of edit types, and the 2010 Docs design also needs a central server. CRDTs suit offline and peer-to-peer editing but carry IDs and markers for deleted text.
  • Beginners can stop after the OT vs CRDT table. Engineers then get a convergence check to run, tests of when the CRDT libraries Yjs 13.6.33 and Automerge 3.5.0 jumble words, byte counts for deletes, and a sync design.

What goes wrong when two people edit the same document at once?

Text disappears. If the editor saves the whole document and the last save wins, whoever saves second quietly overwrites the other person's changes. Nobody sees an error, and collaborative editing exists to stop exactly this.

Picture a common setup. A small team builds a browser-based document editor for other small teams, such as architecture firms writing tender documents together. The setup is made up, but the problem is real.

The first version was simple. Your browser loaded the whole document, you typed, and every few seconds the browser sent the whole document back. The server stored whatever arrived last.

That worked while one person typed at a time. Then a 12-person firm started writing tenders together, and support got 37 tickets in one week saying the same thing: my paragraph vanished.

The team reproduced it with two browser tabs, used by two writers we will call A and B. Follow the arrows from the shared starting version to the box at the end.

How whole-document saves lose a paragraph

Notice the arrow between the two saves: each save replaces the whole document, so the later one erases the earlier one without any warning.

Diagram connections:

  • Both open version 7 to A adds a price line
  • Both open version 7 to B fixes the title
  • A adds a price line to Server saves version 8 (Autosave)
  • B fixes the title to Server saves version 9 (Autosave 2 seconds later)
  • Server saves version 8 to Server saves version 9 (Overwritten by)
  • Server saves version 9 to Price line is gone
  • Both open version 7: Writers A and B load the same text
  • A adds a price line: Exists only in A's browser for now
  • B fixes the title: B's copy has no price line
  • Server saves version 8: A's full document, with the price line
  • Server saves version 9: B's full document, without it
  • Price line is gone: Version 9 silently replaced version 8

Engineers call this a lost update: two people change the same data, and one change silently replaces the other. Locking the document would stop it, but then only one person could type at a time.

The customers wanted to type together, the way people do in Google Docs. So the team needed to merge edits instead of replacing documents. There are two known ways to do that, OT and CRDTs, and this guide follows the team's choice between them.

What is collaborative editing and how does it work?

Collaborative editing is when several people edit one shared document at the same time and every copy ends up the same. It works by sending small edits instead of whole documents, and merging them by fixed rules.

Three ideas make it work. First, each person types into a local copy in their own browser, so typing never waits for the network. This is optimistic editing: you apply your change first and sync it after.

Second, the browser sends each change as an operation, a small description of one edit. An example is: insert 'Rs 40,000' at position 8. A one-letter operation took about 20 bytes in Yjs 13.6.33, while the document may run to many pages.

Third, every copy must merge the same operations into the same text, even when they arrive in different orders. A system that manages this is said to converge: once everyone has every edit, all copies match.

The hard case is concurrent edits. Two edits are concurrent when neither person had seen the other's edit when they made their own. The merge rules must keep both, in a sensible order.

Picture two signboard painters lettering one long shop sign at the same time. One starts at the left end, the other at the right. Both work from the same chalk sketch, and neither can see what the other has just painted.

If the left painter squeezes in an extra word, every letter position the right painter measured from the left edge is now wrong. Collaborative editors face exactly this problem, and OT and CRDTs are two ways to solve it.

Map it part by part: the painters are the writers, the sign is the document, and the chalk sketch is the version both started from. A letter count from the left edge is a position. Where the picture breaks: painters can step back and fix a muddle by eye, but an editor cannot, so its merge rules must get every case right alone.

How does operational transformation (OT) work?

Operational transformation works by rewriting an incoming operation so its position fits your current copy. If someone inserted text before that spot, you shift the position forward by the length of their insert.

Google's engineers described this in September 2010 for the new Docs editor. They wrote that all edits come down to three kinds of change: inserting text, deleting text and applying a style to a range. Their example showed a delete hitting the wrong letters until it was shifted by two characters.

Back on the sign, the right painter planned to start the word SWEETS at letter 14, counted from the left. The left painter added four letters, so the right painter moves his chalk mark four letters along. That move is a transform.

Now try it on the team's editor. The document reads 'Budget: ' and is 8 characters long. Positions count from 0, so position 8 is the end. Writer A types 'Rs 40,000' at position 8, and at the same moment writer B types 'Draft. ' at position 0.

On A's screen, B's insert arrives at position 0, which is before A's text, so it needs no change. On B's screen, A's insert must now land after B's 7 new characters, so it shifts from position 8 to position 15.

The code below handles inserts only, and runs as it is on Python 3. Each insert records who typed it in a site field, which breaks ties when two people insert at exactly the same spot.

python
from dataclasses import dataclass

@dataclass
class Insert:
    pos: int    # where the text goes, counted from the start
    text: str
    site: str   # who typed it; breaks ties at the same spot

def transform(a, b):  # rewrite a so it fits after b is applied
    if b.pos < a.pos or (b.pos == a.pos and b.site < a.site):
        return Insert(a.pos + len(b.text), a.text, a.site)
    return a

def apply(doc, op):
    return doc[:op.pos] + op.text + doc[op.pos:]

edit_a, edit_b = Insert(8, "Rs 40,000", "a"), Insert(0, "Draft. ", "b")
print(apply(apply("Budget: ", edit_a), transform(edit_b, edit_a)))  # A's screen
print(apply(apply("Budget: ", edit_b), transform(edit_a, edit_b)))  # B's screen

What just happened? The if line is the whole idea: an insert that lands earlier pushes your position right by its length. A tie goes to the smaller site name, so 'a' comes before 'b' on every copy.

Both print lines show the same text: Draft. Budget: Rs 40,000. A random test of 20,000 pairs of inserts found no pair where the two screens differed.

Why deletes need their own rules

Deletes break the insert rule. Say the text is 'abcdef', and at the same time A deletes 'cd' while B deletes 'de'. Shift B's delete left by A's length, apply A's delete unchanged, and one screen shows 'af' while the other shows 'ab'.

Both should read 'abf', because the shared letter d may only be deleted once. The open-source OT library ot.js (version 0.0.15, last released in 2014) has full rules for inserts, deletes and retains, which skip over text. It gives 'abf' on both screens, and 5,000 random pairs of its operations all converged in our test.

Styles need rules too. Google's post gives one: a bold range from position 10 to 20 becomes 10 to 23 when three letters are inserted at position 15.

How does Google Docs keep everyone in sync with OT?

In the design Google described in 2010, every change goes through a central server that gives it a revision number. The server's revision log is the one true order, and late changes are transformed against it.

The post lists what each side remembers. Each client, meaning the code running in your browser, keeps four things:

  • The number of the latest revision it received from the server.
  • Changes made locally but not yet sent.
  • Changes sent to the server but not yet acknowledged.
  • Its own current view of the document.

The server keeps the changes it has not processed yet, the complete revision log, and the current document. A client never sends more than one change at a time. New typing waits in the pending list until the server acknowledges the change in flight.

When a change arrives that was based on an old revision, the server transforms it against every change committed since then. It stores the result as the next revision and sends it to the other editors. Here is that path for one of A's edits.

One edit through an OT server

Notice the question box: the server fixes one order and transforms only the edits that arrive late. Revision numbers are examples.

Diagram connections:

  • A types locally to Send edit based on rev 41
  • Send edit based on rev 41 to Is rev 41 still the latest?
  • Is rev 41 still the latest? to Store as revision 43 (Yes)
  • Is rev 41 still the latest? to Transform against rev 42 (No, rev 42 exists)
  • Transform against rev 42 to Store as revision 43
  • Store as revision 43 to Acknowledge to A
  • Store as revision 43 to Send revision 43 to B
  • A types locally: Shown on A's screen at once
  • Send edit based on rev 41: A's browser sends it to the server
  • Is rev 41 still the latest?: Server checks its revision log
  • Transform against rev 42: Shift it past B's newer change
  • Store as revision 43: The next number in the log
  • Acknowledge to A: A's browser may now send its next change
  • Send revision 43 to B: B's browser transforms it if needed

Google listed what this design buys. Typing stays fast because you never wait for the server. Every client has enough information to merge changes in the same deterministic way, meaning the same inputs always give the same result. The server needs no knowledge of each client's state.

The price is the server itself. Every edit to a document must pass through one place that decides the order. If A works offline for a whole day, A's queued edits must be transformed against everything committed meanwhile.

What is a CRDT and how does it merge text?

A CRDT, or conflict-free replicated data type, is a data structure designed so that copies merge on their own. Any two copies that have received the same edits hold the same value, whatever order the edits arrived in. Yjs, for example, holds back an edit until the edits it builds on have arrived.

Shapiro, Preguiça, Baquero and Zawirski defined the idea formally in 2011. They call the guarantee strong eventual consistency: copies that have received the same updates hold equivalent state. Reaching it needs no consensus, meaning the copies never have to stop and agree first.

One sufficient way to get there, in their paper, is to make every pair of concurrent operations commute: they give the same result in either order. Their result also assumes each copy receives an edit only after the edits it builds on. Their trivial example is a counter, where adding 1 then 2 equals adding 2 then 1.

Text is harder, because 'insert at position 8' means different things on different copies. So text CRDTs stop using positions.

Every character gets a permanent ID made of a counter and the writer's site name, such as (3, 'a'). The counter is one more than the biggest counter that writer has seen, a form of the logical clock Lamport published in 1978. A new character is placed after a named character, not at a number.

On the sign, this is like each painter pencilling a tiny mark under every letter they paint. The right painter's note now reads: SWEETS starts after the mark under the last A. Extra letters elsewhere cannot move that anchor.

Deleting works differently too. A CRDT usually keeps a deleted character as a tombstone, a hidden marker. Other edits may still point at that ID, so it cannot simply vanish.

Below is a tiny text CRDT in the style of RGA (Replicated Growable Array), one published design. Each character is a tuple of its ID, the ID it follows, and the letter. The render function turns the set of characters into text.

python
def render(chars, deleted):
    # chars: (id, after_id, letter) tuples; an id is (counter, site)
    kids = {}
    for cid, after, letter in chars:
        kids.setdefault(after, []).append((cid, letter))
    out = []
    def walk(parent):
        for cid, letter in sorted(kids.get(parent, []), reverse=True):
            if cid not in deleted:    # a tombstone hides the letter
                out.append(letter)
            walk(cid)                 # but letters after it keep their place
    walk(None)
    return "".join(out)

base = [((1, "a"), None, "H"), ((2, "a"), (1, "a"), "i")]
bang_a = ((3, "a"), (2, "a"), "!")   # A types ! after the i
quest_b = ((3, "b"), (2, "a"), "?")  # B types ? after the same i
print(render(base + [bang_a, quest_b], set()))  # Hi?!
print(render(base + [quest_b, bang_a], set()))  # Hi?!

What just happened? Sorting by ID means that when A and B both add a character right after 'i', every copy orders them the same way, larger ID first. The ID (3, 'b') beats (3, 'a') on the site name, so both lines print Hi?!, whatever order the edits arrived in.

The other key line is walk(cid), which runs even for deleted letters, so text typed after a deleted character keeps its place. Real libraries add compact storage, fast lookups and network sync, so nobody writes this by hand for production.

What is the difference between OT and CRDT?

OT in the Docs design transforms positions against one central order, while a CRDT gives characters permanent IDs so edits merge in any order. That OT design puts the cleverness in the server's protocol, and a CRDT puts it in the data itself.

OT (as in the 2010 Docs design) and text CRDTs compared
OTCRDT
Where an edit goesA position, like index 8After a character ID, like (2, 'a')
Who decides the orderA central server's revision logNobody: the merge rule is built into the data
Long offline editingPossible, but the queue is transformed against everything sinceNatural: merge whenever you reconnect
Peer to peer, no serverNot in the Docs design, which needs one server to order editsSupported by design
Extra dataNone per character; the server keeps a revision logAn ID per character, plus markers for deleted text
Hardest part to get rightTransform rules for every pair of edit typesOrdering rules, memory use and clean-up
Server work per editTransform, store, relayStore and relay; merging can happen on any copy

Neither is simply better. Google's 2010 editor used OT with a central server. The open-source libraries Yjs and Automerge use CRDTs, and both describe themselves as suited to local-first software, where your own device holds the main copy.

Yjs says it makes no assumptions about your network and needs no central source of truth. It still offers persistence providers that store document updates in a database, because many products want a server copy anyway. Automerge also says it works fine with a central server.

Beginners can stop here: you know why text vanished, how OT and CRDTs each fix it, and the main trade-off. The rest is for engineers, starting with a check you can run on a real library.

How can you check that a CRDT library really converges?

Run two copies of one document in a single script, edit both at once, swap their updates and compare. An update is the library's compact binary message describing edits. The script below uses Yjs 13.6.33, the latest release as of 19 October 2026, tested on Node.js 22.

Save it as check.mjs in an empty folder, run npm install [email protected], then run node check.mjs.

javascript
import * as Y from 'yjs'

const a = new Y.Doc(), b = new Y.Doc()
a.getText('doc').insert(0, 'Budget: ')
Y.applyUpdate(b, Y.encodeStateAsUpdate(a))  // both copies start the same

a.getText('doc').insert(8, 'Rs 40,000')      // A types at the end
b.getText('doc').insert(0, 'Draft. ')        // B types at the start, at once

Y.applyUpdate(a, Y.encodeStateAsUpdate(b))  // swap what each copy knows
Y.applyUpdate(b, Y.encodeStateAsUpdate(a))
console.log(a.getText('doc').toString())
console.log(a.getText('doc').toString() === b.getText('doc').toString())

Expect two lines: Draft. Budget: Rs 40,000, and then true. The same edits merged into that same text in both merge orders on Automerge 3.5.0 too.

A passing check proves convergence for one case only. To find ordering surprises, type whole words at the same spot and delete text, which is what the next section does.

What can go wrong with CRDTs in a text editor?

A correct CRDT converges once every copy has every edit, but converging is not the same as making sense. The main problems are interleaved words, growing metadata and deleted characters that never fully leave.

Interleaved words

A 2019 paper by Kleppmann, Gomes, Mulligan and Beresford showed that some text CRDTs can mix two people's words letter by letter. Every copy agrees on the jumbled result, so the convergence guarantee still holds.

Back to the sign. Imagine both painters are asked to fill the same gap between two words. If each spreads his letters evenly across the gap, their letters alternate, and nobody can read the sign.

The paper found the full problem in Logoot and LSEQ, which give each character a position between its neighbours, like a fraction. RGA showed a lesser form: words can interleave when letters are not typed in order, worst when someone types back to front.

The toy prints Hidcoagt, with the letters alternating, in all 720 possible arrival orders. Typed normally, front to back, it prints Hidogcat with both words whole. That matches the paper's description of RGA.

Real libraries differ here, so test yours. In 200 forward and 200 backward runs on Yjs 13.6.33, words typed at the same spot stayed whole. Yjs records both neighbours of each insert, and its source puts the writer with the lower random client ID first. Automerge 3.5.0 kept forward-typed words whole, but interleaved back-to-front words in all 50 of our runs.

The paper calls back-to-front typing unlikely in real editing, so this is rare. Treat these results as tests on those versions as of 19 October 2026, not promises.

Metadata that grows

Every character carries an ID, and deleted characters leave tombstones behind. A tender rewritten many times can hold more hidden data than visible text. Mature libraries save space by storing a run of characters typed together as one block.

You can measure the block trick in Yjs 13.6.33. Typing 10,000 characters in one run encoded to 10,015 bytes, about 1 byte per character for English text. The same 10,000 characters typed one at a time at random positions took 168,421 bytes, about 17 bytes per character.

Cleaning up deleted characters

Removing a tombstone completely is only safe once every copy has seen the delete. Otherwise a late edit may point at a missing ID, and knowing that every copy has caught up is hard when people stay offline for weeks.

The 2011 CRDT paper describes clean-up that discards an entry only after it has reached every process. It notes that this clean-up stalls while any process has crashed, though correctness does not depend on it.

Yjs takes a middle path, which we read in its source at version 13.6.33. Yjs calls its version of this clean-up garbage collection. With it on, the default, a deleted character's text is dropped but a small marker keeps its ID and length. Neighbouring deleted runs from the same writer then merge into one marker.

Measured on 13.6.33: typing 10,000 characters and deleting them all left a 24-byte update with garbage collection on, and 10,024 bytes with it off. A letter typed concurrently inside the deleted word still survived the merge.

Automerge goes the other way and keeps the full change history by design, so you can view and compare old versions. Plan storage for that history.

How would you design a Google Docs-style editor for a small team?

Use a sync server for each open document, a durable log of operations, regular snapshots and a separate path for presence. Presence means who is online and where their cursor is. That shape works with either OT or a CRDT.

The team picked a CRDT library for the document, for the reason in the next section, and still kept a server. The server checks permissions, stores every edit and relays edits to everyone in the document. Each box below is one part, and the note says what it does.

A small-team collaborative editing setup

Notice the two paths: edits flow through the sync server into the log, while cursors and online lists skip the log entirely.

Diagram connections:

  • Editor in the browser to Connection router (WebSocket)
  • Connection router to Document sync server
  • Document sync server to Permission service (Check role)
  • Document sync server to Presence store (Cursor moves)
  • Document sync server to Edit log (Append edit)
  • Snapshot worker to Edit log (Read edits)
  • Snapshot worker to Snapshot store (Write snapshot)
  • Document sync server to Snapshot store (Load on open)
  • Editor in the browser: Local copy; queues edits while offline
  • Connection router: Sends every user of one doc to one server
  • Document sync server: Applies, stores and relays edits
  • Permission service: Can this person edit, comment or only view?
  • Presence store: Cursors and who is online, in memory only
  • Edit log: Every operation for each document, in order
  • Snapshot worker: Builds snapshots and trims old history
  • Snapshot store: A compact copy every 500 edits

A WebSocket is a connection that stays open, so the server can push each edit the moment it arrives. Sending every user of one document to the same sync server keeps that document's edits in one place.

Here is what happens when writer A opens a tender document after a site visit:

  1. Check permission

    The sync server asks the permission service whether A may edit this document. Viewers receive edits but cannot send them.

  2. Load the latest snapshot

    The server reads the newest snapshot from the snapshot store, say the one taken at edit 12,000.

  3. Replay recent edits

    It applies edits 12,001 onwards from the log and sends the result to A's browser.

  4. Merge offline work

    A typed during the site visit, so A's browser now sends its queued edits. The CRDT merges them with everything A missed.

  5. Join the live stream

    From now on, every new edit and cursor move reaches A over the open connection.

A few details decide whether this holds up in production:

  • Check permissions on every incoming edit, not only when the document opens. Someone removed from a document must stop editing at once.
  • Give each edit an ID made in the browser, so an edit resent after a dropped connection is stored only once.
  • Make undo reverse only your own edits, never a colleague's typing that landed in between.
  • Build named versions, like Tuesday's draft, from snapshots plus the log, so people can look back without slowing live editing.

The load is gentle for a small-team tool. Say 8,000 documents are open at peak, with 3 editors each, sending 2 operations a second (made up). That is 48,000 operations a second, before browsers batch keystrokes into fewer messages.

Should you choose OT or CRDT for a collaborative editor?

Choose a CRDT library when people must edit offline for long periods or without a central server. Choose OT when every edit already flows through one server and your team has transform rules it trusts.

The decision path below starts at the top box. Each question box asks one thing, and the arrow labels are the answers.

Picking a merge method

The team's customers edit on construction sites with weak signal, so the path ended at the first Yes.

Diagram connections:

  • New collaborative editor to Long offline editing?
  • Long offline editing? to Use a CRDT library (Yes)
  • Long offline editing? to Peer to peer, no server? (No)
  • Peer to peer, no server? to Use a CRDT library (Yes)
  • Peer to peer, no server? to Proven OT code in hand? (No)
  • Proven OT code in hand? to Use OT with a server (Yes)
  • Proven OT code in hand? to Use a CRDT library (No, use a library)
  • New collaborative editor: The team's document editor
  • Long offline editing?: Hours or days without a connection
  • Peer to peer, no server?: Devices sync directly with each other
  • Proven OT code in hand?: Tested transform rules for every edit type
  • Use a CRDT library: Test how it orders same-spot inserts
  • Use OT with a server: One server orders every edit

For this team, one requirement settled it. Architects often edit tender papers at building sites with poor signal. The OT prototype stayed in a branch, and a CRDT library went into the editor.

A month after launch, tickets about vanished paragraphs dropped from 37 a week to zero. A new kind of ticket appeared: two people had written the same sentence twice. That is not a merge bug, and comments on the document solve it.

How do you explain collaborative editing in a system design interview?

Start from the lost update, then show how operations, a merge method and a sync server fix it. Name the trade-off between OT and CRDTs plainly, and tie it to one requirement.

  1. Clarify the scope

    Ask how many people edit one document, whether offline editing matters, and what content exists: plain text, tables, images or comments.

  2. Show the naive design failing

    Whole-document saves where the last write wins will lose text. Draw two users and one overwritten save.

  3. Send operations and apply them locally

    Each edit becomes a small operation, applied at once in the browser, then sent to the server over a WebSocket.

  4. Pick the merge method

    Choose OT with a central revision log, or a CRDT with character IDs. Justify it with the offline and server questions.

  5. Add storage and scale

    Describe the append-only edit log, regular snapshots, one sync server per open document, and presence kept in memory.

  6. Cover the edge cases

    Mention permission checks on every edit, duplicate edits after reconnects, undo of your own changes, and version history.

Questions people ask

Does Google Docs use OT or CRDT?
Google described its Docs editor as using operational transformation in a September 2010 engineering blog series. Edits went through a central server that numbered them in a revision log, and each browser transformed incoming changes. That series is the design Google documented publicly, so treat it as the 2010 design rather than a description of today's system.
Is a CRDT better than OT?
Neither is better in every case. CRDTs suit people who edit offline or without a central server, because copies merge in any order. OT adds no data per character and fits well when one server already orders every edit. Most teams pick whichever has a mature, well-tested library for their editor, then test it on their own edge cases.
What does conflict-free mean in a CRDT?
It means concurrent edits never need a person or a server to pick a winner. The data type's merge rule always produces one result, and every copy that has seen the same edits computes that same result. It does not promise the result matches what each person intended.
Can collaborative editing work offline?
Yes. The browser keeps a local copy and queues edits while offline. When it reconnects, it sends the queue, and OT or a CRDT merges it with what others did. CRDTs handle long offline periods more naturally, because they do not depend on a server's position numbers.
How are other people's cursors shown in a real-time collaborative editor?
Each browser sends its cursor position as presence data over the open connection. The server relays it to the other editors and keeps it only in memory. A cursor must also shift when someone inserts text before it, just like an edit does.
How is collaborative editing different from Git?
Git merges sets of changes when you ask it to, and leaves conflicts for a person to resolve by hand. A collaborative editor merges every small edit automatically, within moments. Git suits code that people review before merging, while live editors suit documents people write together at the same time.
Do I need WebSockets for collaborative editing?
Usually yes, or something similar. Edits must reach other people within a fraction of a second, so the server needs a way to push them. A WebSocket keeps one connection open in both directions, which suits many small, frequent messages like keystrokes and cursor moves.

Sources and further reading

Practise this with Lala

Practise