Specific CRDT implementations including LWW-Register, OR-Set, RGA, and collaborative text editing CRDTs
Scope: Specific CRDT implementations, LWW, OR-Set, sequences, collaborative editing Lines: ~370 Last Updated: 2025-10-27 Format Version: 1.0 (Atomic)
Activate this skill when:
Approach: Use timestamps to resolve conflicts (latest wins)
import time
class LWWRegister:
"""Last-Write-Wins Register"""
def __init__(self, replica_id):
self.replica_id = replica_id
self.value = None
self.timestamp = 0
def set(self, value):
"""Set value with current timestamp"""
self.value = value
self.timestamp = time.time()
def merge(self, other):
"""Merge with another replica (latest timestamp wins)"""
result = LWWRegister(self.replica_id)
if other.timestamp > self.timestamp:
result.value = other.value
result.timestamp = other.timestamp
else:
result.value = self.value
result.timestamp = self.timestamp
return result
# Usage
r1 = LWWRegister('A')
r2 = LWWRegister('B')
r1.set('value1') # timestamp: t1
time.sleep(0.001)
r2.set('value2') # timestamp: t2 (t2 > t1)
merged = r1.merge(r2)
print(merged.value) # 'value2' (latest)
Problem: Concurrent writes may be lost
Replica A: set('A') at t=100.001
Replica B: set('B') at t=100.002
Result: 'B' wins (but 'A' is lost!)
Approach: Keep all concurrent values, resolve externally
from typing import Set, Tuple
class MVRegister:
"""Multi-Value Register (keeps concurrent values)"""
def __init__(self, replica_id):
self.replica_id = replica_id
self.values = set() # Set of (value, vector_clock) tuples
def set(self, value, vector_clock):
"""Set value with vector clock"""
# Remove dominated values
self.values = {
(v, vc) for (v, vc) in self.values
if not self._dominates(vector_clock, vc)
}
self.values.add((value, vector_clock.copy()))
def get(self):
"""Get all concurrent values"""
return {v for (v, vc) in self.values}
def merge(self, other):
"""Merge preserving concurrent values"""
result = MVRegister(self.replica_id)
result.values = self.values | other.values
# Remove dominated values
result.values = {
(v1, vc1) for (v1, vc1) in result.values
if not any(
self._dominates(vc2, vc1) and (v1, vc1) != (v2, vc2)
for (v2, vc2) in result.values
)
}
return result
def _dominates(self, vc1, vc2):
"""Check if vc1 dominates vc2"""
return all(vc1.get(k, 0) >= vc2.get(k, 0) for k in vc2) and vc1 != vc2
Approach: Each element tagged with unique ID, can be re-added after remove
import uuid
from typing import Set, Tuple
class ORSet:
"""Observed-Remove Set"""
def __init__(self):
self.elements = {} # element → set of unique IDs
def add(self, element):
"""Add element with unique tag"""
tag = str(uuid.uuid4())
if element not in self.elements:
self.elements[element] = set()
self.elements[element].add(tag)
return tag # Return for tracking
def remove(self, element):
"""Remove element (all current tags)"""
if element in self.elements:
removed_tags = self.elements[element].copy()
del self.elements[element]
return removed_tags
return set()
def lookup(self, element):
"""Check if element exists"""
return element in self.elements and len(self.elements[element]) > 0
def merge(self, other):
"""Merge two OR-Sets"""
result = ORSet()
# Union of all elements and their tags
all_elements = set(self.elements.keys()) | set(other.elements.keys())
for elem in all_elements:
my_tags = self.elements.get(elem, set())
other_tags = other.elements.get(elem, set())
combined_tags = my_tags | other_tags
if combined_tags:
result.elements[elem] = combined_tags
return result
# Usage
set1 = ORSet()
set2 = ORSet()
set1.add('apple')
set2.add('banana')
set1.remove('apple')
set1.add('apple') # Can re-add after remove!
merged = set1.merge(set2)
print(merged.lookup('apple')) # True (re-added)
print(merged.lookup('banana')) # True
Key insight: Remove only affects observed adds (hence "Observed-Remove")
Approach: Each element has unique ID and knows its predecessor
import uuid
from typing import Optional
class RGANode:
def __init__(self, value, timestamp, replica_id):
self.id = (timestamp, replica_id, str(uuid.uuid4()))
self.value = value
self.prev_id = None
self.tombstone = False # For deletion
class RGA:
"""Replicated Growable Array"""
def __init__(self, replica_id):
self.replica_id = replica_id
self.nodes = {} # id → RGANode
self.root_id = None
self.timestamp = 0
def insert_after(self, prev_id, value):
"""Insert value after given position"""
self.timestamp += 1
node = RGANode(value, self.timestamp, self.replica_id)
node.prev_id = prev_id
self.nodes[node.id] = node
return node.id
def insert_at_start(self, value):
"""Insert at beginning"""
self.timestamp += 1
node = RGANode(value, self.timestamp, self.replica_id)
node.prev_id = None
self.root_id = node.id
self.nodes[node.id] = node
return node.id
def delete(self, node_id):
"""Delete by marking as tombstone"""
if node_id in self.nodes:
self.nodes[node_id].tombstone = True
def to_list(self):
"""Convert to list (excluding tombstones)"""
if not self.nodes:
return []
# Build list by following prev_id links
result = []
current_id = self.root_id
while current_id:
node = self.nodes[current_id]
if not node.tombstone:
result.append(node.value)
# Find next node (node where prev_id == current_id)
next_nodes = [
(nid, n) for nid, n in self.nodes.items()
if n.prev_id == current_id and not n.tombstone
]
if next_nodes:
# Resolve conflicts by ID ordering
next_nodes.sort(key=lambda x: x[1].id)
current_id = next_nodes[0][0]
else:
break
return result
def merge(self, other):
"""Merge with another RGA"""
result = RGA(self.replica_id)
result.nodes = {**self.nodes, **other.nodes} # Union of nodes
result.timestamp = max(self.timestamp, other.timestamp)
return result
Approach: Assign each character a unique position identifier
import random
class LogootPosition:
"""Position identifier between existing positions"""
def __init__(self, identifiers):
self.identifiers = identifiers # List of (int, replica_id)
def __lt__(self, other):
return self.identifiers < other.identifiers
def __eq__(self, other):
return self.identifiers == other.identifiers
@staticmethod
def between(pos1, pos2, replica_id):
"""Generate position between pos1 and pos2"""
# Simplified: add random component
new_id = pos1.identifiers + [(random.randint(1, 100), replica_id)]
return LogootPosition(new_id)
class LogootChar:
def __init__(self, position, char):
self.position = position
self.char = char
class LogootDocument:
"""Collaborative text editor using Logoot"""
def __init__(self, replica_id):
self.replica_id = replica_id
self.chars = [] # Sorted list of LogootChar
# Sentinels
self.begin = LogootPosition([(0, 'BEGIN')])
self.end = LogootPosition([(float('inf'), 'END')])
def insert(self, index, char):
"""Insert character at index"""
# Get positions before and after insertion point
pos_before = self.chars[index - 1].position if index > 0 else self.begin
pos_after = self.chars[index].position if index < len(self.chars) else self.end
# Generate position between them
new_pos = LogootPosition.between(pos_before, pos_after, self.replica_id)
# Insert character
logoot_char = LogootChar(new_pos, char)
self.chars.insert(index, logoot_char)
self.chars.sort(key=lambda c: c.position) # Maintain sorted order
return logoot_char
def delete(self, index):
"""Delete character at index"""
if 0 <= index < len(self.chars):
deleted = self.chars.pop(index)
return deleted
def to_string(self):
"""Get document as string"""
return ''.join(c.char for c in self.chars)
def merge(self, other):
"""Merge with another document"""
result = LogootDocument(self.replica_id)
result.chars = sorted(
self.chars + other.chars,
key=lambda c: c.position
)
return result
Simplified Yjs approach:
class YjsChar:
def __init__(self, char, clock, client_id):
self.char = char
self.id = (client_id, clock)
self.left_id = None # ID of left neighbor
self.right_id = None # ID of right neighbor
self.deleted = False
class YjsText:
"""Simplified Yjs-like text CRDT"""
def __init__(self, client_id):
self.client_id = client_id
self.clock = 0
self.items = {} # id → YjsChar
def insert(self, index, char):
"""Insert character at index"""
self.clock += 1
items_list = self._to_list()
yjs_char = YjsChar(char, self.clock, self.client_id)
if index > 0:
yjs_char.left_id = items_list[index - 1].id
if index < len(items_list):
yjs_char.right_id = items_list[index].id
self.items[yjs_char.id] = yjs_char
return yjs_char.id
def delete(self, index):
"""Delete character at index"""
items_list = self._to_list()
if 0 <= index < len(items_list):
items_list[index].deleted = True
def _to_list(self):
"""Reconstruct ordered list"""
# Build from left/right relationships
result = []
for item in self.items.values():
if not item.deleted:
result.append(item)
# Sort by causal relationships and IDs
result.sort(key=lambda x: x.id)
return result
def to_string(self):
return ''.join(item.char for item in self._to_list())
class ORMap:
"""Map with OR-Set semantics for keys"""
def __init__(self):
self.keys = ORSet() # Keys as OR-Set
self.values = {} # key → CRDT value
def set(self, key, value_crdt):
"""Set key to CRDT value"""
self.keys.add(key)
if key not in self.values:
self.values[key] = value_crdt
else:
# Merge with existing value
self.values[key] = self.values[key].merge(value_crdt)
def remove(self, key):
"""Remove key"""
self.keys.remove(key)
# Keep value for potential re-add
def get(self, key):
"""Get value for key"""
if self.keys.lookup(key):
return self.values.get(key)
return None
def merge(self, other):
"""Merge two OR-Maps"""
result = ORMap()
result.keys = self.keys.merge(other.keys)
# Merge values for all keys
all_keys = set(self.values.keys()) | set(other.values.keys())
for key in all_keys:
if key in self.values and key in other.values:
result.values[key] = self.values[key].merge(other.values[key])
elif key in self.values:
result.values[key] = self.values[key]
else:
result.values[key] = other.values[key]
return result
Problem: Deleted items accumulate as tombstones
Solution: Periodic GC when safe
class RGAWithGC(RGA):
def garbage_collect(self, min_vector_clock):
"""Remove tombstones that all replicas have seen"""
self.nodes = {
nid: node for nid, node in self.nodes.items()
if not node.tombstone or not self._all_replicas_seen(node, min_vector_clock)
}
def _all_replicas_seen(self, node, min_vc):
"""Check if all replicas have seen this deletion"""
node_time, node_replica, _ = node.id
return min_vc.get(node_replica, 0) >= node_time
Approach: Merge consecutive inserts from same replica
class CompressedRGA:
"""RGA with run-length compression"""
def __init__(self, replica_id):
self.replica_id = replica_id
self.runs = [] # List of (values, id, prev_id)
def insert_run(self, prev_id, values):
"""Insert multiple values as single run"""
run_id = self._generate_id()
self.runs.append((values, run_id, prev_id))
return run_id
| Use Case | CRDT Type | Why | |----------|-----------|-----| | Configuration value | LWW-Register | Simple, last write wins acceptable | | Feature flags | OR-Set | Enables/disables need conflict handling | | Shopping cart | OR-Set or OR-Map | Items can be added/removed by multiple devices | | Like counter | PN-Counter | Increments/decrements commute | | Collaborative text | Logoot or Yjs | Preserves user intent in concurrent edits | | Chat messages | RGA | Insertion order matters | | User presence | LWW-Register or G-Set | Recent status most relevant |
distributed-systems-crdt-fundamentals - CRDT basicsdistributed-systems-vector-clocks - Causality trackingdistributed-systems-conflict-resolution - Conflict handlingdistributed-systems-eventual-consistency - Consistency modelsLast Updated: 2025-10-27