Skip to content

CRDT Types

Uni includes a comprehensive library of Conflict-free Replicated Data Types (CRDTs) for distributed, eventually-consistent data synchronization.

Overview

CRDTs are data structures that can be replicated across multiple nodes, updated independently, and merged without conflicts. They guarantee eventual consistency without coordination.

┌─────────────────────────────────────────────────────────────────────────────┐
│                            CRDT TYPES IN UNI                                 │
├──────────────────┬──────────────────┬───────────────────────────────────────┤
│    COUNTERS      │      SETS        │           REGISTERS & MAPS            │
├──────────────────┼──────────────────┼───────────────────────────────────────┤
│ • GCounter       │ • GSet           │ • LWWRegister                         │
│   (Grow-only)    │   (Grow-only)    │   (Last-Writer-Wins)                  │
│                  │ • ORSet          │ • LWWMap                              │
│                  │   (Add-wins)     │   (Per-key LWW)                       │
├──────────────────┴──────────────────┴───────────────────────────────────────┤
│                          SEQUENCES                                           │
├─────────────────────────────────────────────────────────────────────────────┤
│ • Rga (Replicated Growable Array) - Ordered sequences with insert/delete    │
└─────────────────────────────────────────────────────────────────────────────┘

CrdtMerge Trait

All CRDT types implement the CrdtMerge trait:

pub trait CrdtMerge {
    /// Merge another instance into self.
    /// Must satisfy: commutativity, associativity, idempotency.
    fn merge(&mut self, other: &Self);
}

Properties Guaranteed: - Commutativity: a.merge(b) == b.merge(a) - Associativity: (a.merge(b)).merge(c) == a.merge(b.merge(c)) - Idempotency: a.merge(a) == a


GCounter (Grow-only Counter)

A counter that can only be incremented. Each node maintains its own count, and the total is the sum across all nodes.

Use Cases

  • View counts
  • Like counters
  • Event tallies
  • Any monotonically increasing metric

API

use uni_crdt::GCounter;

let mut counter = GCounter::new();

// Increment for a specific actor/node
counter.increment("node_a", 5);
counter.increment("node_b", 3);
counter.increment("node_a", 2);

// Get total value
assert_eq!(counter.value(), 10);

// Get per-actor count
assert_eq!(counter.actor_count("node_a"), 7);
assert_eq!(counter.actor_count("node_b"), 3);

Merge Behavior

let mut a = GCounter::new();
a.increment("A", 5);
a.increment("B", 2);

let mut b = GCounter::new();
b.increment("B", 7);  // B has higher count here
b.increment("C", 3);

a.merge(&b);

// Takes maximum for each actor
assert_eq!(a.actor_count("A"), 5);
assert_eq!(a.actor_count("B"), 7);  // max(2, 7)
assert_eq!(a.actor_count("C"), 3);
assert_eq!(a.value(), 15);

GSet (Grow-only Set)

A set that only supports additions. Elements can never be removed.

Use Cases

  • Append-only logs
  • Immutable tag collections
  • User IDs that have seen content
  • One-time events

API

use uni_crdt::GSet;

let mut set: GSet<String> = GSet::new();

// Add elements
set.add("apple".to_string());
set.add("banana".to_string());

// Check membership
assert!(set.contains(&"apple".to_string()));
assert!(!set.contains(&"cherry".to_string()));

// Iterate elements
for elem in set.elements() {
    println!("{}", elem);
}

assert_eq!(set.len(), 2);

Merge Behavior

let mut a: GSet<i32> = GSet::new();
a.add(1);
a.add(2);

let mut b: GSet<i32> = GSet::new();
b.add(2);
b.add(3);

a.merge(&b);

// Union of both sets
assert!(a.contains(&1));
assert!(a.contains(&2));
assert!(a.contains(&3));
assert_eq!(a.len(), 3);

ORSet (Observed-Remove Set)

A set that supports both add and remove operations. Uses unique tags to track element provenance. Concurrent add + remove results in the element being present (add-wins semantics).

Use Cases

  • Collaborative editing (selected items)
  • User preferences
  • Shopping carts
  • Any set with deletion support

API

use uni_crdt::ORSet;

let mut set: ORSet<String> = ORSet::new();

// Add elements (returns a unique tag)
let tag = set.add("apple".to_string());
set.add("banana".to_string());

// Remove elements
set.remove(&"apple".to_string());

// Check membership
assert!(!set.contains(&"apple".to_string()));
assert!(set.contains(&"banana".to_string()));

// Get visible elements
let elements = set.elements();

Add-Wins Semantics

let mut a: ORSet<String> = ORSet::new();
a.add("apple".to_string());

let mut b = a.clone();
b.remove(&"apple".to_string());  // Remove on replica B

// Concurrent add on replica A
a.add("apple".to_string());  // New tag created

a.merge(&b);

// Add wins! The new tag in 'a' wasn't tombstoned in 'b'
assert!(a.contains(&"apple".to_string()));

LWWRegister (Last-Writer-Wins Register)

A single-value register where conflicts are resolved by timestamp. The value with the highest timestamp wins.

Use Cases

  • User profile fields
  • Document titles
  • Configuration values
  • Any single-value property

API

use uni_crdt::LWWRegister;

let mut reg = LWWRegister::new("initial".to_string(), 100);

// Set with timestamp
reg.set("newer".to_string(), 110);
assert_eq!(reg.get(), "newer");

// Older timestamp is ignored
reg.set("older".to_string(), 105);
assert_eq!(reg.get(), "newer");  // Still "newer"

// Get current timestamp
assert_eq!(reg.timestamp(), 110);

Merge Behavior

let a = LWWRegister::new("A".to_string(), 100);
let b = LWWRegister::new("B".to_string(), 110);

let mut merged = a.clone();
merged.merge(&b);

// B wins (higher timestamp)
assert_eq!(merged.get(), "B");

LWWMap (Last-Writer-Wins Map)

A map where each key is managed by an independent LWWRegister. Supports put and remove operations.

Use Cases

  • User preferences (key-value)
  • Entity properties
  • Configuration maps
  • JSON-like documents

API

use uni_crdt::LWWMap;

let mut map: LWWMap<String, i32> = LWWMap::new();

// Put key-value pairs with timestamp
map.put("a".to_string(), 1, 100);
map.put("b".to_string(), 2, 110);

// Get values
assert_eq!(map.get(&"a".to_string()), Some(&1));
assert_eq!(map.get(&"b".to_string()), Some(&2));

// Remove with timestamp (tombstone)
map.remove(&"a".to_string(), 120);
assert_eq!(map.get(&"a".to_string()), None);

// Iterate keys
for key in map.keys() {
    println!("{}", key);
}

Merge Behavior

let mut a: LWWMap<String, i32> = LWWMap::new();
a.put("a".to_string(), 1, 100);

let mut b: LWWMap<String, i32> = LWWMap::new();
b.put("a".to_string(), 2, 110);  // Higher timestamp
b.put("b".to_string(), 3, 100);

a.merge(&b);

// Per-key LWW resolution
assert_eq!(a.get(&"a".to_string()), Some(&2));  // b's value wins
assert_eq!(a.get(&"b".to_string()), Some(&3));

Rga (Replicated Growable Array)

An ordered sequence supporting insertion and deletion at any position. Used for collaborative text editing and ordered collections.

Use Cases

  • Collaborative text editing
  • Ordered lists
  • Comment threads
  • Version histories

API

use uni_crdt::Rga;

let mut rga: Rga<char> = Rga::new();

// Insert elements (returns ID for future reference)
let id1 = rga.insert(None, 'H', 1);           // Insert at beginning
let id2 = rga.insert(Some(id1), 'e', 2);      // Insert after id1
let id3 = rga.insert(Some(id2), 'l', 3);
let id4 = rga.insert(Some(id3), 'l', 4);
rga.insert(Some(id4), 'o', 5);

// Convert to vector
let text: String = rga.to_vec().into_iter().collect();
assert_eq!(text, "Hello");

// Delete element (tombstone)
rga.delete(id2);  // Remove 'e'
let text: String = rga.to_vec().into_iter().collect();
assert_eq!(text, "Hllo");

Concurrent Insert Resolution

let mut a: Rga<char> = Rga::new();
let id0 = a.insert(None, 'A', 1);

let mut b = a.clone();

// Concurrent inserts after id0
a.insert(Some(id0), 'B', 2);  // timestamp 2
b.insert(Some(id0), 'C', 3);  // timestamp 3

a.merge(&b);

let result: String = a.to_vec().into_iter().collect();
// C comes before B (higher timestamp)
assert_eq!(result, "ACB");

Dynamic CRDT Wrapper

For storage and serialization, Uni provides a dynamic Crdt enum:

use uni_crdt::{Crdt, GCounter, CrdtMerge};

// Wrap any CRDT type
let mut gc = GCounter::new();
gc.increment("actor1", 42);
let crdt = Crdt::GCounter(gc);

// Serialize to MessagePack
let bytes = crdt.to_msgpack().unwrap();

// Deserialize
let decoded = Crdt::from_msgpack(&bytes).unwrap();

// Merge works on wrapped types
let mut a = Crdt::GCounter(GCounter::new());
let b = Crdt::GCounter(GCounter::new());
a.merge(&b);

Supported Variants

Variant Type Description
Crdt::GCounter GCounter Grow-only counter
Crdt::GSet GSet<String> Grow-only set
Crdt::ORSet ORSet<String> Observed-remove set
Crdt::LWWRegister LWWRegister<serde_json::Value> Last-writer-wins register
Crdt::LWWMap LWWMap<String, serde_json::Value> Last-writer-wins map
Crdt::Rga Rga<String> Replicated growable array

Choosing the Right CRDT

Use Case CRDT Type Why
Page views, likes GCounter Only increments, no conflict
Tags (append-only) GSet Never need removal
User selections ORSet Need add/remove with add-wins
Single property value LWWRegister Simple conflict resolution
Key-value properties LWWMap Per-key conflict resolution
Collaborative text Rga Ordered with concurrent edits

Best Practices

1. Choose Appropriate Granularity

// Bad: Single LWWRegister for entire document
let doc = LWWRegister::new(large_json, timestamp);

// Good: LWWMap with fine-grained keys
let mut props = LWWMap::new();
props.put("title".to_string(), title_value, ts);
props.put("author".to_string(), author_value, ts);

2. Use Logical Timestamps

// Use hybrid logical clocks or Lamport timestamps
// Not wall-clock time alone (clock skew issues)
let hlc_timestamp = hlc.now();
register.set(value, hlc_timestamp);

3. Consider Tombstone Growth

// ORSet and LWWMap accumulate tombstones
// Periodically compact if history not needed
// Or use garbage collection with causal stability

4. Match Semantics to Requirements

// Need "add wins"? Use ORSet
// Need "remove wins"? Use 2P-Set or LWW with remove timestamp
// Need both? Consider custom CRDT composition

Next Steps