CRDTs: Theory and Practice

April 15, 2025

CRDTs, or Conflict-Free Replicated Data Types, have fascinated me since the start of my software career. They're amazing little tools with awesome properties, but they haven't quite caught on. They're stuck in the realm of academic curiosities.

If this is your first time hearing about CRDTs, here's a 30-second pitch:

The 30-Second Pitch

CRDTs are a set of rules. If you can make your state strictly adhere to the rules you get a bunch of cool features:

  • State can sync itself between multiple devices.
  • Clients can make offline changes and seamlessly merge after reconnecting.
  • Updates are instant. You're notifying the server something changed, not asking.
  • States always converge. No rollbacks, no corruption (Strong Eventual Consistency).
  • Everything is real-time by default.
  • You don't need a single source of truth. Apps can run peer-to-peer or federated.

It's important to note CRDTs don't force you to use those features. You can still run a central server, you can still keep a single source of truth. It's all about raising the ceiling of what's possible, what you get out of the box.

Why Would You Need This?

You don't. It only comes up in latency-critical contexts like Google Docs, games, and distributed systems. Maybe the occasional over-engineered notes app. But even if we don't "need" them, it leaves a lot on the table.

If someone nails the right abstraction, your app, your entire app, gets superpowers. It opens the door to features we don't imagine because they're hard, dangerous, and nobody else is doing it. A general-purpose CRDT could make those features easy, and may even change the field of software.

Hang with me.

The Theory (Broad Strokes)

Anything that implements these rules is a CRDT:

  • Commutativity: Order doesn't matter. You can apply operations in any order and get the same result.
  • Associativity: Operations can be grouped arbitrarily and still reach the same output.
  • Idempotence: Applying the same operation a bunch of times is the same as applying it once.

There are tons of specialized CRDTs with clever data structures and mental models, but they all boil down to one basic approach: the set union.

{ a, b } ∪ { a, c } = { a, b, c }

Set union satisfies all three properties. These days, innovations come from fine-tuning domains to create optimized data representations and by finding neat ways to preserve core properties while aligning closer with user expectations.

A classic example is the Last-Write-Wins Element Set. It models a single variable. There's a set of operations and each has a time (lamport timestamp) and a value.

{
(time: 1, value: a),
(time: 2, value: b),
(time: 3, value: c),
}

Someone changes the value? That's a new operation: (time: 4, value: d). Put it in the set.

You get the current value by finding the one with the biggest timestamp. If there's a tie, use a tiebreaker. Maybe take the biggest one, or the smallest one. It doesn't matter so long as everyone agrees in advance.

That's it. That's enough to sync a value. You're not gonna build your next app with this approach (although some have), but it illustrates the point. Variations support deletions using a second set of delete operations. You could put it in the operation as (type: delete, ...) just as well. It hardly matters.

There are two interesting things to note about the LWW-E-Set:

  • Garbage collection: a strict approach grows unbounded, but we only care about the "newest" element and the rest can be deleted. Knowing our domain gives us the means to optimize the representation.
  • CRDTs are not values: this is a crucial point and one of the most common trip-ups. CRDTs are not your application state. They derive your application state.

Operation Logs

Let's take a step back from CRDTs and look at a different model. For any application, state is a function of all its inputs. If you mocked out every file and network request, every timer and event, you could play it back and get exactly the same output. Software isn't magic; it's deterministic math. All the funky bits come from non-deterministic inputs.

Time-traveling debuggers work on this idea. State management systems like The Elm Architecture and Redux work on this idea. Some databases work on this idea. It's a powerful model that shows up everywhere you look.

// Example app managing a todo list
let operations = vec![
Action::Add(task1.clone()),
Action::Add(task2.clone()),
Action::MarkCompleted(task1.id),
Action::Remove(task2.id),
];
derive_state(operations); // vec![task1]

In the example someone adds two tasks to a todo list, marks one completed, and removes another. A derive_state() function plays through the operations and computes the state, a list with a single completed task.

The op-log pattern is powerful enough to represent any data structure. Derive graphs, tables, KV stores, trees, programs, even other operation logs. It's a universal format.

Now, notice the boundary between state management and product domain: state management treats the operation log as the source of truth, while the application couldn't care less. It only needs a task list. The operation log is an internal detail. A more elaborate example is a database whose tables materialize from an internal journal. You don't read the journal, you might not even know it's there. You only care about the tables.

The same is true for CRDTs. The set union is an internal detail; what matters is the derived value. Trying to fit your entire application domain in the set is like Sisyphus rolling a boulder up a hill: heroically pointless. Recognize that merging updates and deriving state are different phases and only the first needs the properties of a CRDT.

There's a lot of overlap between operation logs and CRDTs. Most well-known CRDTs define orderings and transformations on an operation tree, flattening it to an operation log. Once you have an order, you have an operation log, and you can derive a state.

Linearizing the Tree

The big difference between operation logs and CRDTs is concurrent edits. Operation logs are append-only lists. The fanciest logs have read replicas with elected write leaders, but there's still one transactor pushing entries at the end.

CRDTs aren't lists. They don't have a linear flow because there's no single writer. More than one actor can append at a time. (Concurrency is the whole point.) Log-style CRDTs behave more like trees. When two actors append an item, it creates a branch:

┌────────┐
┌────►│ Op 2.1 │
│ └────────┘
┌──────┐ ┌────┴─┐
│ Init ├────►│ Op 1 │
└──────┘ └────┬─┘
│ ┌────────┐
└────►│ Op 2.2 │
└────────┘

In this example the log is created (empty list), a single operation is added, then two actors try to append at the same time.

To derive state we need to flatten it into a list, but how do we get the right order? Deterministically sort the branches? Do we need to drop some of the updates? The solution depends on what you're modeling. Text documents have wildly different answers than replicated tables, for instance.

This is the crux of the CRDT problem: log linearization requires domain knowledge.

There are creative ways around it. Git solves the problem by baking the concept of branches into the user experience. Instead of creating a single causal history, it derives one log per actor and branch pair (heads + main). It guarantees total local order because you're the only one appending to your partition. Forks are permanent without user intervention (git merge) which creates a new operation (commit) in the target log.

I'm gonna make someone angry, but yes, git runs on a CRDT. The .git/objects store is a merkle tree which is a type of G-Set, arguably the most famous example. This is what makes git so powerful. Yes it has conflicts, but those happen when you create new operations (merge commits), and there's no CRDT rule about that. So long as each upstream has its own partition, you're guaranteed a conflict-free merge into the object store.

Let's be clear: this is a horrible user experience. We all have war stories. I'm not advocating we present merge conflicts to our users. Still, it's one of the most well-known programs in the space and worth studying.

Narrow Domains

I said log linearization requires domain knowledge. Most of today's CRDTs are "narrow", meaning they specialize on a specific domain or data structure. They've hard-coded rules for resolving concurrent edits that mostly do what you expect.

The rules apply while deriving state. It doesn't change objects in the underlying CRDT. Rules only care about concurrent operations, since sequential edits already have an order.

The process of linearizing concurrent edits boils down to 3 kinds of changes:

  • Reorder: Sort the branches to find the most "desirable" outcome. If one person updates a paragraph and another person deletes it, a delete-biased ordering should put the delete last.
  • Transform: Post-process branches when concurrent edits are inserted before them. If text insertions use a string index, prepending text invalidates the address. You need to shift it to account for the new offset.
  • Prune: Some kinds of edits are mutually exclusive, like creating two tables with the same name. If reordering and transformation can't fix it, you need to pick one and ignore the other. It might require pruning downstream operations too.

These changes are designed for the harsh environment of fast-changing tightly coupled state. A perfect example is collaborative text editing where any change can affect the entire document. Log post-processing is essential in those domains because concurrent edits and semantic conflicts are all but guaranteed.

In traditional contexts, you probably won't see it as often. Sure, two users might change the same row or operate on data linked by foreign key constraint, but most edits are independent. Just because two edits happened concurrently doesn't mean they're in conflict. They may be fine as-is.

General Domains

There's no such thing as a general purpose CRDT, at least not today. Narrow domains dominate the field. There are exciting efforts like Automerge and Y.js, but you won't choose these technologies as your primary database any time soon.

Nothing stops us from deriving SQL tables instead of text documents or JSON structures. An operation log can derive anything. Pre-processing rules can implement ACID properties, with a pedantic caveat on durability if concurrent operations require pruning the transaction.

A system like this would have really neat properties. You'd get multi-writer replication, stream processing, offline edits, local-first applications, time-travel queries, a complete audit log, live backups, and real-time updates out of the box.

Let's go a step further. Notice that operations are immutable. The derive step generates new state, it doesn't change the input. Large blocks of immutable data are ideal for distributed systems. Stuff 'em in S3, put a cache in front. This is starting to look like Datomic.

Why stop at SQL tables? Blockchains (pardon the dirty word) have shown that specialized ecosystems can plug into a central operation log and extend it with novel capabilities. Providing hooks for log post-processing could see embedded narrow domain CRDTs in a general domain the same way you'd install a Postgres extension.

There's so much unlocked potential if only we could nail the abstraction.

Why Hasn't Anyone Done This?

I don't know. If I can speculate, and it's my blog so I can, it's a combination of:

  • Obscurity: There aren't a lot of good resources on CRDTs out there. It's stuck in academic papers and narrow domains. It takes a long time to tease out the patterns and longer to see a full picture.
  • Maturity: The first papers came out in 2007. Use cases were already saturated by Operational Transform. It's a young field and it took a while to gain traction.
  • Demographics: Today, most interest comes from local-first hobbyists. An outsider might dismiss the technology as catering to a niche audience. (An exception for Distributed Systems engineers. They love this stuff. But it's typically in the context of augmenting existing technologies, not replacing them.)
  • Demand: Your boss won't ask for this. Customers won't ask for this. Companies that do (Notion, Figma) base it as their core differentiator. Here, technology drives the demand, not the other way around.
  • Difficulty: It's hard. "Step 1: build a new database." If people are thinking about this, only a tiny subset have the intent and means to execute.

The more people who know about it, the more likely someone goes and builds it and we all move forward. Hopefully this post mades a dent.

Resources

Research papers are still the most best way to get specifics, but there are plenty of excellent resources that cover the broader ideas. Here are some highlights:

  • Turning the Database Inside Out: Martin Kleppmann is famous for his work in the area (and many other good reasons). All his talks are excellent and worth watching.
  • Data Laced with History: An absolute goldmine of knowledge. One of the best resources on the internet attacking the problem from a novel angle.
  • 5000x Faster CRDTs: Seph Gentle is a legend in the adjacent space of Operational Transform. His post does a deep dive on text CRDT performance.
  • Ink & Switch: A research group focused on building local-first applications. They're one of the leaders in the space.
  • Y.js: Arguably the most popular CRDT library. It's a great way to explore the concepts in practice.