Consistency Models: The Spectrum of Distributed Guarantees
Purpose: Formal specifications of correctness for concurrent and distributed systems, defining what behaviors are legal when multiple processes access shared state.
Core Insight: Stronger consistency = easier reasoning but lower performance. Trade consistency for availability (CAP), latency (PACELC), or scalability.
Table of Contents
- Foundational Concepts
- Strong Consistency
- Weak Consistency
- Causal Consistency
- Eventual Consistency
- Trade-offs and Theorems
- Stack-Specific Implementations
- Integration Points
Foundational Concepts
What is a Consistency Model?
Consistency Model: Contract between system and programmer specifying:
- Which operation orderings are legal
- Which values reads can return
- Guarantees about visibility of writes
Not the same as:
- ACID Consistency: Application-level invariants
- Consensus: Agreement protocol for replicated state
History-Based Definitions
History: Sequence of operations across all processes.
Operation: Tuple (process, operation_type, object, value, timestamp)
- Example:
(P1, write, x, 5, t1),(P2, read, x, 5, t2)
Legal History: History allowed by consistency model.
Ordering Relations
Program Order (→_p):
- Operations by same process, in program order
- Example: If P1 writes x then reads y,
write(x) →_p read(y)
Happens-Before (→):
- Transitive closure of program order + causal dependencies
- Foundation for causal consistency
Real-Time Order:
- If op1 completes before op2 starts (wall clock)
- Used in linearizability definition
Replication and Consistency
Why Replicate?:
- Fault Tolerance: Survive failures
- Scalability: Distribute load
- Latency: Serve from nearby replica
Replication Challenge: Keep replicas consistent while:
- Handling concurrent updates
- Tolerating network delays/partitions
- Maintaining performance
Strong Consistency
Linearizability (Atomic Consistency)
Strongest consistency model for concurrent objects.
Definition (Herlihy & Wing, 1990): System is linearizable if:
- Linearization Points: Each operation appears to execute instantaneously at some point between invocation and response
- Real-Time Ordering: If op1 completes before op2 starts, op1’s linearization point precedes op2’s
- Sequential Specification: Linearized history respects object’s sequential specification
Intuition: System behaves like single copy with atomic operations.
Example (Linearizable):
P1: write(x, 1) ---------------|P2: |----- read(x) returns 1 -------|P3: |---- read(x) returns 1 ---|
Linearization: write(x,1) → read(x)_P2 → read(x)_P3Example (Not Linearizable):
P1: write(x, 1) -----|P2: |---- read(x) returns 0 --|P3: |--- read(x) returns 1 ---|
P2's read overlaps write but returns old value,while P3's later read sees new value.Not linearizable: no consistent ordering.Properties:
- Composable: Linearizable objects compose into linearizable system
- Non-Blocking: Some operation can always complete (with infinite threads)
- Local: Can verify per-object independently
Implementation: Consensus-based replication (Paxos, Raft), single leader.
Sequential Consistency
Weaker than linearizability: Relaxes real-time constraint.
Definition (Lamport, 1979): System is sequentially consistent if:
- Program Order: Operations of each process appear in program order
- Global Order: All processes see operations in same total order
- No requirement to respect real-time order between processes
Intuition: There exists some sequential interleaving consistent with each process’s program order.
Example (Sequentially Consistent but not Linearizable):
P1: write(x, 1) ---|P2: write(y, 1) ---|P3: |--- read(y) returns 1, read(x) returns 0 ---|P4: |--- read(x) returns 1, read(y) returns 0 ---|
Sequential order: write(y,1) → write(x,1) (for P3's view) write(x,1) → write(y,1) (for P4's view)Contradiction! Not sequentially consistent.
But if:P3: read(y)=1, read(x)=1P4: read(x)=1, read(y)=1Then sequential: write(x,1) → write(y,1) → readsProperties:
- Not composable (unlike linearizability)
- Easier to implement than linearizability (no real-time constraint)
Implementation: Total Store Order (TSO), some cache coherence protocols.
Strict Serializability
Combines serializability (transactions) + linearizability (real-time).
Definition: Transactions are:
- Serializable: Equivalent to some serial execution
- Linearizable: Respect real-time order
Used In: Spanner (Google), FaunaDB (external consistency).
Weak Consistency
PRAM (Pipelined RAM)
Guarantee: Writes by single process seen in order by all others. No ordering between different processes’ writes.
Definition:
- Writes from same process must be seen in order
- Writes from different processes can be seen in any order
Example:
P1: write(x, 1), write(x, 2)P2: read(x) = 1, read(x) = 2 ✓ (ordered)P3: read(x) = 2, read(x) = 1 ✗ (violates PRAM)
P1: write(x, 1)P2: write(y, 1)P3: read(x)=0, read(y)=1P4: read(y)=0, read(x)=1 ✓ (different processes, any order OK)Use Case: Processor memory models (see Memory Models appendix).
Processor Consistency
Stronger than PRAM: Adds constraint that all processes agree on order of writes to same location.
Definition:
- PRAM + writes to same location are totally ordered
- Different locations can be reordered
Implementation: Many multiprocessor cache coherence protocols.
Monotonic Reads
Guarantee: If process reads value v, subsequent reads won’t return older values.
Formally: If read(x) = v_i precedes read(x) in process’s program order, second read returns v_j where v_j at least as recent as v_i.
Example:
P1: read(x) = 5 (version 5)P1: read(x) = 7 ✓ (version 7 ≥ 5)P1: read(x) = 3 ✗ (violates monotonic reads)Use Case: Session consistency in distributed databases.
Monotonic Writes
Guarantee: Writes by same process executed in order.
Formally: If write(x, v1) precedes write(x, v2) in process’s program order, all replicas apply writes in that order.
Example:
P1: write(x, 1)P1: write(x, 2)
All replicas eventually have: x = 2No replica sees: x = 2 then x = 1Use Case: Social media feeds (posts appear in order user created them).
Read Your Writes
Guarantee: Process’s reads reflect its prior writes.
Formally: If process writes x, subsequent reads by that process return written value (or newer).
Example:
P1: write(x, 5)P1: read(x) = 5 ✓ (sees own write)P1: read(x) = 3 ✗ (doesn't see own write)Implementation: Session stickiness (route to same replica), write-through cache.
Writes Follow Reads
Guarantee: Writes after read are ordered after values read.
Formally: If process reads x = v1 then writes y = v2, any process reading y = v2 will see x ≥ v1.
Use Case: Comment threads (comment sees post it replies to).
Causal Consistency
Key Insight: Preserve causally related operations, allow concurrent operations to be seen in different orders.
Definition
Causal Consistency:
- Operations causally related must be seen in same order by all processes
- Concurrent operations can be seen in different orders
Causality (→):
- Program Order: op1 →_p op2 in same process → op1 → op2
- Reads-From: write(x, v) → read(x, v)
- Transitivity: op1 → op2 and op2 → op3 → op1 → op3
Concurrent: op1 || op2 if neither op1 → op2 nor op2 → op1.
Examples
Causal:
P1: write(x, 1)P2: read(x) = 1, write(y, 2) // causally depends on write(x,1)P3: read(y) = 2, read(x) = 1 ✓ (sees cause before effect)P4: read(y) = 2, read(x) = 0 ✗ (sees effect without cause)Concurrent Writes (OK in Causal):
P1: write(x, 1)P2: write(x, 2) // concurrent with P1's writeP3: read(x) = 1, read(x) = 2P4: read(x) = 2, read(x) = 1 // different order OK (concurrent)Vector Clocks
Mechanism: Track causality using vector clocks.
Vector Clock: Array V where V[i] = number of events process i has seen.
Rules:
- Local event: V[i]++
- Send message: Include V in message
- Receive message with V_m: V[i] = max(V[i], V_m[i]) for all i, then V[i]++
Comparison:
- V1 < V2 if ∀i: V1[i] ≤ V2[i] and ∃j: V1[j] < V2[j] (V1 causally precedes V2)
- V1 || V2 if neither V1 < V2 nor V2 < V1 (concurrent)
Example:
P1: [1,0,0] write(x,1)P2: [0,1,0] → receive from P1 → [1,2,0] write(y,2)P3: [0,0,1] → receive from P2 → [1,2,2] read(y)=2
P3 knows write(y,2) causally depends on write(x,1)Must see write(x,1) before applying write(y,2)Causal+ Consistency
Extension: Causal consistency + convergence for concurrent writes.
Combines:
- Causal consistency (ordering)
- CRDTs or conflict resolution for concurrent updates
Example Systems: COPS, Eiger, ChainReaction.
Eventual Consistency
Weakest useful model: If no new updates, all replicas eventually converge.
Definition
Eventual Consistency:
- Eventual Delivery: Update delivered to all replicas eventually
- Convergence: Replicas with same updates have same state
- Termination: All methods terminate
No guarantees about:
- How long until convergence
- Intermediate states
- Order of updates seen
Examples
Allowed:
P1: write(x, 1)P2: read(x) = 0 (stale, but eventually...)P2: read(x) = 1 (converged)Anomalies (allowed temporarily):
P1: write(x, 1), write(y, 1)P2: read(y) = 1, read(x) = 0 // sees y before x (causal violation)Strong Eventual Consistency
Stronger variant: CRDTs provide strong eventual consistency.
Guarantees:
- Eventual Delivery: Same as eventual consistency
- Strong Convergence: Replicas with same updates have same state (deterministic merge)
- No coordination: Updates applied locally without synchronization
Conflict-Free Replicated Data Types (CRDTs):
- State-based (CvRDTs): Merge states, must be semilattice
- Operation-based (CmRDTs): Commutative operations
Examples:
- G-Counter: Grow-only counter (increment only)
- PN-Counter: Positive-negative counter (increment/decrement)
- LWW-Register: Last-writer-wins register (timestamp-based)
- OR-Set: Observed-remove set (add/remove elements)
CRDT Example: G-Counter
State: Map[ProcessID, Int]
increment(id): state[id]++
value(): return sum(state.values)
merge(other): for each id: state[id] = max(state[id], other.state[id])Merge is:
- Commutative: merge(A, B) = merge(B, A)
- Associative: merge(merge(A, B), C) = merge(A, merge(B, C))
- Idempotent: merge(A, A) = A
Result: Strong eventual consistency - all replicas converge to same count.
Trade-offs and Theorems
CAP Theorem
Theorem (Brewer, 2000; Gilbert & Lynch, 2002):
“A distributed system can provide at most 2 of 3 guarantees:
- Consistency (C): Linearizability
- Availability (A): Every request receives response
- Partition Tolerance (P): System continues despite network partitions”
In Practice: Networks partition → must choose C or A.
CP Systems (Sacrifice Availability):
- HBase, MongoDB (with majority writes)
- Consensus-based: Paxos, Raft
AP Systems (Sacrifice Consistency):
- Cassandra (tunable), DynamoDB
- Eventual consistency, CRDTs
CA Systems (Mythical):
- Only work in single-node or perfect network (unrealistic)
PACELC
Extension of CAP (Abadi, 2012):
“If Partition, trade Availability vs Consistency; Else (no partition), trade Latency vs Consistency.”
PA/EL: Prioritize availability and latency (Cassandra, DynamoDB) PC/EC: Prioritize consistency (HBase, VoltDB) PA/EC: Prioritize availability during partition, consistency otherwise PC/EL: Prioritize consistency during partition, latency otherwise
Harvest and Yield
Alternative to CAP (Fox & Brewer, 1999):
- Yield: Probability of completing request
- Harvest: Fraction of data reflected in response
Trade-off: Can sacrifice harvest (incomplete data) to maintain yield (availability).
Example: Search engine returns partial results (reduced harvest) but always responds (high yield).
Stack-Specific Implementations
Rust: Causally Consistent Store
use std::collections::HashMap;use std::cmp::Ordering;
/// Vector clock for causality tracking#[derive(Clone, Debug, PartialEq, Eq)]struct VectorClock { clocks: HashMap<String, u64>,}
impl VectorClock { fn new() -> Self { VectorClock { clocks: HashMap::new(), } }
fn increment(&mut self, process_id: &str) { *self.clocks.entry(process_id.to_string()).or_insert(0) += 1; }
fn merge(&mut self, other: &VectorClock) { for (proc, &time) in &other.clocks { let entry = self.clocks.entry(proc.clone()).or_insert(0); *entry = (*entry).max(time); } }
fn happens_before(&self, other: &VectorClock) -> bool { let mut less_than = false; let mut less_equal = true;
for (proc, &time) in &self.clocks { let other_time = other.clocks.get(proc).copied().unwrap_or(0); if time > other_time { less_equal = false; } else if time < other_time { less_than = true; } }
for (proc, _) in &other.clocks { if !self.clocks.contains_key(proc) { less_than = true; } }
less_than && less_equal }
fn concurrent(&self, other: &VectorClock) -> bool { !self.happens_before(other) && !other.happens_before(self) }}
/// Versioned value with vector clock#[derive(Clone, Debug)]struct VersionedValue<T> { value: T, version: VectorClock,}
/// Causally consistent key-value storestruct CausalStore<T: Clone> { process_id: String, data: HashMap<String, Vec<VersionedValue<T>>>, clock: VectorClock,}
impl<T: Clone> CausalStore<T> { fn new(process_id: String) -> Self { CausalStore { process_id, data: HashMap::new(), clock: VectorClock::new(), } }
fn write(&mut self, key: String, value: T) { // Increment local clock self.clock.increment(&self.process_id);
let versioned = VersionedValue { value, version: self.clock.clone(), };
// Add to version history self.data.entry(key).or_insert_with(Vec::new).push(versioned); }
fn read(&mut self, key: &str) -> Option<Vec<T>> { // Return all concurrent versions let versions = self.data.get(key)?;
// Filter to maximal elements (not dominated by others) let mut maximal = Vec::new();
for v1 in versions { let mut is_maximal = true; for v2 in versions { if v1.version.happens_before(&v2.version) { is_maximal = false; break; } } if is_maximal { maximal.push(v1.value.clone()); } }
Some(maximal) }
fn merge(&mut self, other: &CausalStore<T>) { // Merge vector clocks self.clock.merge(&other.clock);
// Merge data for (key, other_versions) in &other.data { let local_versions = self.data.entry(key.clone()).or_insert_with(Vec::new);
for other_version in other_versions { // Add if not dominated by existing version let mut should_add = true; for local_version in local_versions.iter() { if other_version.version.happens_before(&local_version.version) { should_add = false; break; } }
if should_add { local_versions.push(other_version.clone()); } } } }}
/// CRDT: G-Counter (grow-only counter)#[derive(Clone, Debug)]struct GCounter { counts: HashMap<String, u64>,}
impl GCounter { fn new() -> Self { GCounter { counts: HashMap::new(), } }
fn increment(&mut self, process_id: String) { *self.counts.entry(process_id).or_insert(0) += 1; }
fn value(&self) -> u64 { self.counts.values().sum() }
fn merge(&mut self, other: &GCounter) { for (proc, &count) in &other.counts { let entry = self.counts.entry(proc.clone()).or_insert(0); *entry = (*entry).max(count); } }}
/// CRDT: PN-Counter (increment/decrement)#[derive(Clone, Debug)]struct PNCounter { increments: GCounter, decrements: GCounter,}
impl PNCounter { fn new() -> Self { PNCounter { increments: GCounter::new(), decrements: GCounter::new(), } }
fn increment(&mut self, process_id: String) { self.increments.increment(process_id); }
fn decrement(&mut self, process_id: String) { self.decrements.increment(process_id); }
fn value(&self) -> i64 { self.increments.value() as i64 - self.decrements.value() as i64 }
fn merge(&mut self, other: &PNCounter) { self.increments.merge(&other.increments); self.decrements.merge(&other.decrements); }}TypeScript: Eventually Consistent Store with CRDTs
/** * Last-Writer-Wins Register CRDT */class LWWRegister<T> { private value: T | null = null; private timestamp: number = 0; private node_id: string;
constructor(nodeId: string) { this.node_id = nodeId; }
set(value: T): void { const now = Date.now(); // Handle clock skew with node ID tiebreaker if (now > this.timestamp) { this.value = value; this.timestamp = now; } }
get(): T | null { return this.value; }
merge(other: LWWRegister<T>): void { if ( other.timestamp > this.timestamp || (other.timestamp === this.timestamp && other.node_id > this.node_id) ) { this.value = other.value; this.timestamp = other.timestamp; } }
getState(): { value: T | null; timestamp: number; nodeId: string } { return { value: this.value, timestamp: this.timestamp, nodeId: this.node_id, }; }}
/** * Observed-Remove Set CRDT */class ORSet<T> { private elements = new Map<T, Set<string>>(); private nodeId: string; private counter = 0;
constructor(nodeId: string) { this.nodeId = nodeId; }
add(element: T): void { const tag = `${this.nodeId}:${this.counter++}`; if (!this.elements.has(element)) { this.elements.set(element, new Set()); } this.elements.get(element)!.add(tag); }
remove(element: T): void { // Remove element by clearing its tags this.elements.delete(element); }
has(element: T): boolean { const tags = this.elements.get(element); return tags !== undefined && tags.size > 0; }
values(): T[] { return Array.from(this.elements.keys()).filter( elem => this.elements.get(elem)!.size > 0 ); }
merge(other: ORSet<T>): void { // Union of all element-tag pairs for (const [element, otherTags] of other.elements.entries()) { if (!this.elements.has(element)) { this.elements.set(element, new Set()); } const localTags = this.elements.get(element)!; for (const tag of otherTags) { localTags.add(tag); } } }}
/** * Vector clock implementation */class VectorClock { private clocks = new Map<string, number>();
increment(processId: string): void { const current = this.clocks.get(processId) || 0; this.clocks.set(processId, current + 1); }
merge(other: VectorClock): void { for (const [proc, time] of other.clocks.entries()) { const localTime = this.clocks.get(proc) || 0; this.clocks.set(proc, Math.max(localTime, time)); } }
happensBefore(other: VectorClock): boolean { let strictlyLess = false; let allLessOrEqual = true;
const allProcs = new Set([ ...this.clocks.keys(), ...other.clocks.keys(), ]);
for (const proc of allProcs) { const thisTime = this.clocks.get(proc) || 0; const otherTime = other.clocks.get(proc) || 0;
if (thisTime > otherTime) { allLessOrEqual = false; } else if (thisTime < otherTime) { strictlyLess = true; } }
return strictlyLess && allLessOrEqual; }
concurrent(other: VectorClock): boolean { return ( !this.happensBefore(other) && !other.happensBefore(this) ); }
clone(): VectorClock { const cloned = new VectorClock(); cloned.clocks = new Map(this.clocks); return cloned; }}
/** * Eventually consistent key-value store */class EventualStore<T> { private data = new Map<string, LWWRegister<T>>(); private nodeId: string;
constructor(nodeId: string) { this.nodeId = nodeId; }
set(key: string, value: T): void { if (!this.data.has(key)) { this.data.set(key, new LWWRegister<T>(this.nodeId)); } this.data.get(key)!.set(value); }
get(key: string): T | null { const register = this.data.get(key); return register ? register.get() : null; }
merge(other: EventualStore<T>): void { for (const [key, otherRegister] of other.data.entries()) { if (!this.data.has(key)) { this.data.set(key, new LWWRegister<T>(this.nodeId)); } this.data.get(key)!.merge(otherRegister); } }}PHP: Session Consistency Implementation
<?php
namespace Consistency;
use Illuminate\Support\Facades\Redis;use Illuminate\Support\Facades\Cache;
/** * Session consistency: Read-your-writes guarantee */class SessionConsistentStore{ private string $sessionId; private int $version = 0;
public function __construct(string $sessionId) { $this->sessionId = $sessionId; $this->loadVersion(); }
/** * Write with version tracking */ public function write(string $key, mixed $value): void { $this->version++;
$data = [ 'value' => $value, 'version' => $this->version, 'timestamp' => microtime(true), ];
// Write to all replicas foreach ($this->getReplicas() as $replica) { $this->writeToReplica($replica, $key, $data); }
// Store session version $this->saveVersion(); }
/** * Read with version checking (read-your-writes) */ public function read(string $key): mixed { $replicas = $this->getReplicas();
foreach ($replicas as $replica) { $data = $this->readFromReplica($replica, $key);
if ($data && $data['version'] >= $this->version) { // Replica has version at least as recent as session return $data['value']; } }
// Fallback: read from any replica (eventual consistency) foreach ($replicas as $replica) { $data = $this->readFromReplica($replica, $key); if ($data) { return $data['value']; } }
return null; }
private function getReplicas(): array { return config('database.replicas', ['localhost']); }
private function writeToReplica( string $replica, string $key, array $data ): void { Redis::connection($replica)->hset( "data:{$key}", 'value', serialize($data) ); }
private function readFromReplica( string $replica, string $key ): ?array { $raw = Redis::connection($replica)->hget("data:{$key}", 'value');
return $raw ? unserialize($raw) : null; }
private function loadVersion(): void { $this->version = (int) Cache::get( "session_version:{$this->sessionId}", 0 ); }
private function saveVersion(): void { Cache::put( "session_version:{$this->sessionId}", $this->version, now()->addHours(24) ); }}
/** * CRDT: LWW-Element-Set */class LWWSet{ private array $added = []; private array $removed = [];
public function add(mixed $element): void { $timestamp = microtime(true); $this->added[$element] = $timestamp;
// Remove from removed set if present unset($this->removed[$element]); }
public function remove(mixed $element): void { $timestamp = microtime(true); $this->removed[$element] = $timestamp; }
public function contains(mixed $element): bool { $addedTime = $this->added[$element] ?? 0; $removedTime = $this->removed[$element] ?? 0;
// Element present if added more recently than removed return $addedTime > $removedTime; }
public function elements(): array { return array_filter( array_keys($this->added), fn($elem) => $this->contains($elem) ); }
public function merge(LWWSet $other): void { // Merge added timestamps (keep maximum) foreach ($other->added as $elem => $time) { if (!isset($this->added[$elem]) || $time > $this->added[$elem]) { $this->added[$elem] = $time; } }
// Merge removed timestamps (keep maximum) foreach ($other->removed as $elem => $time) { if (!isset($this->removed[$elem]) || $time > $this->removed[$elem]) { $this->removed[$elem] = $time; } } }}
/** * Quorum-based consistency (tunable) */class QuorumStore{ private int $replicationFactor; private int $readQuorum; private int $writeQuorum;
public function __construct( int $replicationFactor = 3, int $readQuorum = 2, int $writeQuorum = 2 ) { $this->replicationFactor = $replicationFactor; $this->readQuorum = $readQuorum; $this->writeQuorum = $writeQuorum;
// Ensure R + W > N for strong consistency assert($readQuorum + $writeQuorum > $replicationFactor); }
public function write(string $key, mixed $value): bool { $version = time(); $replicas = $this->selectReplicas($key); $successes = 0;
foreach ($replicas as $replica) { if ($this->writeToReplica($replica, $key, $value, $version)) { $successes++; }
if ($successes >= $this->writeQuorum) { return true; } }
return false; // Failed to reach write quorum }
public function read(string $key): mixed { $replicas = $this->selectReplicas($key); $responses = [];
foreach ($replicas as $replica) { $data = $this->readFromReplica($replica, $key); if ($data !== null) { $responses[] = $data; }
if (count($responses) >= $this->readQuorum) { break; } }
if (count($responses) < $this->readQuorum) { throw new \Exception('Failed to reach read quorum'); }
// Return value with highest version usort($responses, fn($a, $b) => $b['version'] <=> $a['version']);
return $responses[0]['value']; }
private function selectReplicas(string $key): array { // Consistent hashing to select replicas // Simplified: just return configured replicas return config('database.replicas', []); }
private function writeToReplica( string $replica, string $key, mixed $value, int $version ): bool { try { Redis::connection($replica)->hmset("data:{$key}", [ 'value' => serialize($value), 'version' => $version, ]); return true; } catch (\Exception $e) { return false; } }
private function readFromReplica(string $replica, string $key): ?array { try { $data = Redis::connection($replica)->hgetall("data:{$key}"); if (empty($data)) { return null; }
return [ 'value' => unserialize($data['value']), 'version' => (int) $data['version'], ]; } catch (\Exception $e) { return null; } }}Integration Points
With Consensus
- Consensus provides linearizability: Replicated state machine → strongest consistency
- Trade-off: Consensus sacrifices availability during partitions
With Actor Model
- Eventual consistency: Natural for actor systems (async messaging)
- Causal consistency: Preserve message causality in distributed actors
With Distributed Systems
- Replication strategy: Consistency model determines replication protocol
- CAP trade-offs: Choose consistency level based on application needs
Further Reading
Papers
- Lamport (1979) - “How to Make a Multiprocessor Computer That Correctly Executes Multiprocess Programs”
- Herlihy & Wing (1990) - “Linearizability: A Correctness Condition for Concurrent Objects”
- Gilbert & Lynch (2002) - “Brewer’s Conjecture and the Feasibility of Consistent, Available, Partition-Tolerant Web Services”
- Shapiro et al. (2011) - “Conflict-Free Replicated Data Types”
Books
- Kleppmann - “Designing Data-Intensive Applications”
- Tanenbaum & Van Steen - “Distributed Systems: Principles and Paradigms”
End of Consistency Models Appendix