Abdul Samad SiddiquiSenior AI Engineer · Karachi

Collaborative editing works because changes become operations

Last-write-wins and locking both fail for real-time text collaboration. The fix is turning every keystroke into an operation the server can order and transform, here's how, and what it isn't.

Diagram of two cursors sending labeled insert and delete operation strips across dashed lines to a central sequencer that orders and transforms them.

Two people type into the same document at the same time. What should happen? The obvious answers both fail, and understanding why is the fastest way to understand how real collaborative editors actually work.

Why the obvious answers don’t work

Last-write-wins, whoever’s save lands last overwrites everyone else’s changes, is the simplest possible approach, and it loses data constantly. If you type a sentence while I’m typing a different sentence in the same paragraph, one of us simply disappears. That’s not an edge case; it’s the normal case the moment two people type concurrently.

Locking, only one person can edit at a time, avoids the data loss but destroys the thing that makes collaborative editing worth having. Nobody wants to wait for a lock to type a sentence. It turns “collaborative” back into “take turns.”

The fix: don’t send state, send operations

The actual solution is to stop thinking about documents as state that gets overwritten, and start thinking about edits as operations that get applied. Instead of “here is my new version of paragraph three,” a client sends “insert the character ‘x’ at position 47” or “delete three characters starting at position 12.” Those operations get sent to a server, which is responsible for putting them in a consistent order and, critically, transforming later operations against earlier ones so they still make sense once applied.

That transformation step is the hard part, and it’s what the technique is named for: Operational Transformation (OT). If I insert a character at position 10 and, concurrently, you delete a character at position 5, my insert’s target position needs to shift once your delete has been applied first, otherwise it lands in the wrong place. OT is the algorithm for computing that shift so concurrent operations compose correctly regardless of the order they arrive in.

Google Docs is the product that popularized OT at scale and made “real-time collaborative text editing actually works” a mainstream expectation.

A correction I need to make

It’s a common simplification to describe Figma’s multiplayer system as OT, or as using CRDTs (Conflict-free Replicated Data Types, the other major family of approaches to this problem). Neither is quite right. Figma has been explicit that their system is a centralized, custom-built architecture inspired by CRDT ideas, not a textbook CRDT implementation, and not OT either. It’s its own thing, designed around their specific data model (a scene graph of design objects, not a linear text buffer), with a central server that’s still in the loop rather than the fully decentralized, server-optional model that “true” CRDTs are usually associated with. Worth being precise about, since OT and CRDTs solve the same category of problem with meaningfully different trade-offs, and Figma doesn’t map cleanly onto either.

What this means for picking a backend

If you’re building collaborative editing today, you’re not implementing OT or CRDTs from scratch most of the time, you’re choosing between existing approaches, and the right one depends on your constraints more than on which technique is “better”:

  • Raw WebSockets + Redis gives you full control over the ordering and transformation logic, at the cost of building and maintaining that logic yourself. Makes sense if your data model is unusual enough that an off-the-shelf library won’t fit it well.
  • Managed sync platforms (hosted real-time sync services) hand you conflict resolution, presence, and offline support as a product, in exchange for less control and a dependency on someone else’s infrastructure and pricing.
  • Hocuspocus / Yjs and similar CRDT-based libraries give you a well-tested CRDT implementation you can self-host, with strong offline support, at the cost of adapting your data model to how the CRDT represents state.

None of these is a universal winner. Raw WebSockets make sense when you need control and have the team to maintain it. A CRDT library makes sense when offline-first behavior matters more than fine-grained control. A managed platform makes sense when shipping fast matters more than owning the infrastructure. The question worth asking isn’t “which is best”, it’s which trade-off matches what you’re actually building.

Sources and further reading