Skip to main content

Chapter 4: State-Based CRDTs

Overview

State-Based CRDTs (CvRDTs) merge complete states between replicas. They are simpler to implement but can have larger message sizes due to exchanging full state information.

G-Counter Implementation

The Grow-only Counter is one of the simplest CRDTs, supporting only increment operations.

JavaScript Implementation

class GCounter {
constructor(replicaId) {
this.replicaId = replicaId;
this.counters = new Map();
this.counters.set(replicaId, 0);
}

increment() {
const current = this.counters.get(this.replicaId) || 0;
this.counters.set(this.replicaId, current + 1);
}

get value() {
let sum = 0;
for (const count of this.counters.values()) {
sum += count;
}
return sum;
}

merge(other) {
for (const [replicaId, count] of other.counters) {
const current = this.counters.get(replicaId) || 0;
this.counters.set(replicaId, Math.max(current, count));
}
}

toJSON() {
return {
replicaId: this.replicaId,
counters: Object.fromEntries(this.counters)
};
}

static fromJSON(data) {
const counter = new GCounter(data.replicaId);
counter.counters = new Map(Object.entries(data.counters));
return counter;
}
}

Usage Example

// Create counters for two replicas
const counterA = new GCounter('A');
const counterB = new GCounter('B');

// Increment on replica A
counterA.increment();
counterA.increment();

// Increment on replica B
counterB.increment();

// Merge states
counterA.merge(counterB);
counterB.merge(counterA);

console.log(counterA.value); // 3
console.log(counterB.value); // 3

PN-Counter Implementation

The Positive-Negative Counter supports both increment and decrement operations.

JavaScript Implementation

class PNCounter {
constructor(replicaId) {
this.replicaId = replicaId;
this.positive = new GCounter(replicaId);
this.negative = new GCounter(replicaId);
}

increment() {
this.positive.increment();
}

decrement() {
this.negative.increment();
}

get value() {
return this.positive.value - this.negative.value;
}

merge(other) {
this.positive.merge(other.positive);
this.negative.merge(other.negative);
}

toJSON() {
return {
replicaId: this.replicaId,
positive: this.positive.toJSON(),
negative: this.negative.toJSON()
};
}

static fromJSON(data) {
const counter = new PNCounter(data.replicaId);
counter.positive = GCounter.fromJSON(data.positive);
counter.negative = GCounter.fromJSON(data.negative);
return counter;
}
}

G-Set Implementation

The Grow-only Set only supports adding elements.

JavaScript Implementation

class GSet {
constructor() {
this.elements = new Set();
}

add(element) {
this.elements.add(element);
}

has(element) {
return this.elements.has(element);
}

get size() {
return this.elements.size;
}

merge(other) {
for (const element of other.elements) {
this.elements.add(element);
}
}

toJSON() {
return {
elements: Array.from(this.elements)
};
}

static fromJSON(data) {
const set = new GSet();
set.elements = new Set(data.elements);
return set;
}
}

Advantages and Disadvantages

Advantages

  • Simple Implementation: Easy to understand and implement
  • Deterministic Merging: Merge operations are straightforward
  • Reliable: Less prone to implementation errors

Disadvantages

  • Large Message Sizes: Full state must be exchanged
  • Memory Usage: Each replica stores complete state information
  • Network Overhead: Synchronization can be expensive for large states

When to Use State-Based CRDTs

State-based CRDTs are ideal when:

  • State size is manageable
  • Network bandwidth is not a major constraint
  • Simplicity is more important than performance
  • You need reliable, deterministic merging

Next Steps

In the next chapter, we'll explore operation-based CRDTs and their trade-offs.