Under the Hood
Consistency

Conflict resolution & distributed transactions

Once more than one node can accept a write, two of them can change the same thing at once — and something has to decide what the truth becomes. This lesson covers how concurrent writes are even detected (why wall-clock timestamps lie, and what version vectors do instead), the resolution strategies from the data-losing simplicity of last-write-wins up to the mathematical guarantees of CRDTs, and then the other half of the problem: making one operation atomic across several nodes. Why two-phase commit is correct but blocking and avoided at scale, and why sagas trade atomicity for availability with compensating actions.

Conflict resolution & distributed transactions

The topologies lesson ended on a warning it deferred: the moment you leave single-leader — multi-leader, leaderless, or an offline-first client — two nodes can accept writes to the same data concurrently, and when those writes meet, the system faces a question single-leader never asks: which write is the truth, or are they both? This lesson is the two hard halves of that question. First, conflict resolution: reconciling concurrent writes to the same data. Second, distributed transactions: making a single operation atomic across data that lives on different nodes. Both are the price of distributing state, and both are why staying single-leader is worth so much.

First problem: how do you even know two writes conflict?

Before resolving a conflict you must detect one, and the naive detector is broken. The obvious idea — "compare wall-clock timestamps, the later one is newer" — fails because clocks lie. Different machines' clocks drift and are skewed relative to each other (NTP keeps them close, not identical), so a write that actually happened second can carry an earlier timestamp than one that happened first. Ordering distributed events by wall-clock time is unreliable, and building correctness on it is a classic distributed-systems bug.

The fix is logical clocks — counters that capture causality rather than time. A version vector (or vector clock) keeps a per-replica counter so the system can tell, for any two writes, whether one causally preceded the other (one built on the other — keep the later) or they were truly concurrent (neither saw the other — a genuine conflict to resolve). This is the same causality from the consistency-models lesson, made into a mechanism: version vectors are how a system knows two writes are concurrent instead of guessing from timestamps.

Resolving the conflict

Once you know two writes are concurrent, four strategies, from lossy-simple to correct-complex:

  • Last-write-wins (LWW). Pick one by timestamp (or any deterministic tiebreak), discard the other. Trivial to implement and Cassandra's default — and it silently loses data: the discarded write just vanishes, with no error, no trace. If two users concurrently edit a field, one edit is gone. LWW is acceptable only when losing a concurrent write is genuinely fine (a last-seen timestamp, a cache entry) and never when the writes are independently valuable.
  • Application-level merge. Surface the conflict and let code or the user reconcile — the git-merge model ("both changed this; here are both versions"). Correct, but pushes work onto the app and sometimes the human.
  • Version vectors with siblings. The datastore keeps both conflicting versions ("siblings") and hands them to the application on the next read to merge deliberately, instead of silently dropping one. Dynamo/Riak do this — no data lost, resolution deferred to someone who understands the data.
  • CRDTs (Conflict-free Replicated Data Types). Data structures designed so that concurrent updates always merge deterministically, with no coordination and no lost writes — the merge is baked into the type's math (it's associative, commutative, idempotent, so replicas converge regardless of order). A grow-only counter, an add/remove set, a collaborative-text sequence: each has a CRDT that guarantees every replica reaches the same state once they've exchanged updates. CRDTs are how Google Docs-style collaborative editing and modern offline-sync engines avoid conflicts by construction. The cost is that not every data model has a natural CRDT, and they carry metadata overhead — but where they fit, they're the strongest answer: convergence as a mathematical property, not a hope.

Increment each replica a few times while they're offline, then sync. With last-write-wins one replica's increments silently vanish; with a CRDT counter both are preserved and the replicas converge to the true total. Same edits, one loses data.

Conflict strategy — a shared counter, two offline replicas
Replica A (last write)+0
Replica B +0

Once two writes are concurrent, something must resolve them. Last-write-wins is trivial and Cassandra's default — and it silently loses data: the discarded write just vanishes. A CRDT is a data type designed so concurrent updates always merge deterministically (the merge is associative, commutative, idempotent), so replicas converge with no coordination and no lost writes — how collaborative editors work. Not every model has a natural CRDT, but where one fits, convergence is a mathematical guarantee, not a hope.

Second problem: one operation, many nodes

The other half. A single logical action must sometimes touch data on different nodes atomically — a cross-shard transfer, or an operation spanning several services — and you want the ACID all-or-nothing guarantee across all of them: either every part commits, or none does.

Two-phase commit (2PC) is the classic protocol. A coordinator drives two rounds:

  1. Prepare — ask every participant "can you commit this?" Each does the work, durably prepares, locks what's needed, and replies yes (a promise it can commit) or no.
  2. Commit — if all said yes, the coordinator tells everyone "commit"; if any said no, "abort." Participants finalize accordingly.

2PC is correct — it genuinely gives atomicity across nodes. But it has a crippling weakness explored in the callout, and it's slow (two round trips plus a durable write at each participant) and it reduces availability (any participant being down blocks the whole transaction). This is why high-scale systems mostly avoid distributed transactions rather than optimize them.

Sagas are the availability-friendly alternative. Instead of one atomic transaction across nodes, a saga is a sequence of local transactions, each with a defined compensating action that undoes it. Book flight → charge card → reserve hotel; if the hotel step fails, run the compensations in reverse — refund the card, cancel the flight. No distributed locks, no coordinator holding everyone hostage, each step is a normal local commit. The price: no isolation (intermediate states are visible — for a moment the card is charged but the hotel isn't booked) and the burden of writing correct compensations for every step. Sagas trade the strict atomicity of 2PC for availability and are the backbone of long-running cross-service workflows — the distributed-patterns stage builds them properly alongside the outbox pattern.

Go deeper

Check yourself

Answer out loud, as if an interviewer asked. If you hand-wave, reread that section.

  1. Why can you not reliably order two writes on different machines by their wall-clock timestamps? What does a version vector let you determine that a timestamp cannot?
  2. Rank last-write-wins, application merge, version-vector siblings, and CRDTs by how much data they can lose. Explain precisely how LWW loses a write with no error, and when it is nonetheless acceptable.
  3. What guarantee does a CRDT provide that the other strategies do not, and what property of its merge operation makes that guarantee hold regardless of update order? Give an example data type.
  4. Describe the two phases of 2PC. Then explain the blocking problem: what state is a participant in after voting yes, and why does a coordinator crash freeze it?
  5. Contrast 2PC and sagas on atomicity, isolation, and availability. What does a saga use instead of a rollback, and what visible anomaly do you accept in exchange?
  6. Fable resolves no write conflicts at all. Explain why single-leader makes that true, how the settlement advisory lock prevents rather than resolves the one hazard, and why money data specifically could never use LWW if the app went offline-first.