Back to DAG

Write Conflict Resolution

databases

How Write Conflicts Are Resolved

When multiple leaders or nodes accept concurrent writes to the same data, conflicts arise. The system must determine which value "wins" or how to merge them. Different strategies trade off simplicity, correctness, and data preservation.

Last-Write-Wins (LWW)

The simplest strategy: attach a timestamp to each write, and the write with the highest timestamp wins. All replicas converge to the same value deterministically. LWW is used by Cassandra and DynamoDB by default.

Problem: LWW silently discards data. If two users concurrently update different fields of the same row, one update is lost entirely. Clock skew between nodes can cause a "later" write (by wall-clock) to lose to an "earlier" one with a higher timestamp.

Custom Application-Level Merge

The application provides a conflict handler that receives both versions and produces a merged result. For example, a shopping cart conflict might take the union of items from both carts. This preserves all data but requires application-specific logic.

CRDTs (Conflict-free Replicated Data Types)

CRDTs are data structures that mathematically guarantee convergence without coordination. Any order of applying operations yields the same final state. Key CRDTs include:

  • G-Counter (Grow-only Counter): Each node maintains its own counter. The total is the sum across all nodes. To merge, take the max per node.
  • PN-Counter (Positive-Negative Counter): Two G-Counters—one for increments, one for decrements. Value = sum(increments) - sum(decrements).
  • LWW-Register: Stores a value with a timestamp. Merge picks the value with the highest timestamp. Simple but loses concurrent writes.
  • OR-Set (Observed-Remove Set): Each element has a unique tag. Add associates a new tag, remove deletes specific tags. An element is in the set if any of its tags exist. This correctly handles concurrent add/remove.

Operational Transformation (OT)

Used by Google Docs. Each edit is an operation (insert char at position X, delete char at position Y). When concurrent operations arrive, they are transformed against each other so that applying them in any order produces the same result.

Tombstones for Deletes

In replicated systems, you cannot simply delete a record—another replica might re-introduce it during merge. Instead, a tombstone marks the record as deleted. During merge, the tombstone takes precedence over the value (or not, depending on policy). Tombstones must eventually be garbage collected.

Real-Life: CRDTs in Production

Real-World Example

CRDTs are used in major distributed systems:

  • Riak (KV store): Supports built-in CRDT types: counters, sets, maps, registers, and flags. When concurrent writes occur, Riak automatically merges them using CRDT semantics rather than requiring application-level conflict resolution.
  • Redis Enterprise: Implements CRDTs for its active-active geo-replication. Counters, sets, and strings are automatically merged across datacenters.
  • Figma: Uses a CRDT-based data structure to enable real-time collaborative design. Each user's edits are local operations that merge without conflicts.
  • Apple Notes / iCloud: Uses CRDTs to synchronize document edits across devices, handling offline edits gracefully.
  • Automerge / Yjs: Open-source CRDT libraries for building collaborative applications. Yjs powers many real-time collaborative editors.

Example: G-Counter in a distributed voting system Three servers each count votes locally: Node A has 150, Node B has 200, Node C has 180. Total = 530. When nodes sync, they exchange their local counts. Each node takes max(local, remote) per peer. After sync, all nodes agree: A=150, B=200, C=180, total=530. No votes are lost or double-counted.

CRDT Merge: G-Counter

G-Counter: each node keeps its own count, merge = max per node Before sync Node A {A:5, B:0, C:0} value() = 5 Node B {A:3, B:8, C:0} value() = 11 Node C {A:3, B:0, C:4} value() = 7 merge: max per node After sync Node A {A:5, B:8, C:4} value() = 17 Node B {A:5, B:8, C:4} value() = 17 Node C {A:5, B:8, C:4} value() = 17 All nodes converge to the same state: {A:5, B:8, C:4} = 17 No data is lost. No coordination was needed during writes.
Step 1 of 3