The question this answers
Two replicas accepted different writes to the same key. What decides the outcome?
None is implied. A write conflict is the *absence* of an ordering, not a fault. The only guarantee available without coordination is that the conflict can be detected (given causal metadata) and that every replica will reach the same answer if the resolution rule is deterministic and commutative. Detection and convergence are separate properties, and neither implies the answer is the one a user wanted.
Everything below is bought to hold this sentence. "Strongly consistent" with no scope attached is a slogan, not a guarantee — read what it actually covers, and what it explicitly does not.
A replica knows the versions it holds and the causal metadata attached to each. From that it can determine whether one version descends from the other or whether they are concurrent. It does not know which write the user considers more important, whether both were intentional, or whether the two writers knew about each other outside the system. Every one of those is application knowledge the replica is structurally unable to obtain.
A node knows its own state and the messages that arrived. Everything else is inference from evidence that was already stale. "B has not replied in five seconds" is knowledge; "B is down" is a decision — and usually the bug.
The scene, precisely
A cluster runs two replicas that both accept writes — a multi-leader deployment, a leaderless quorum store, an offline-capable mobile client syncing later, or simply two datacentres. The link between them fails. Both keep serving, because that is why they exist (Why Replicate: What a Second Copy Buys You, Multi-Leader Replication: Accepting Writes in More Than One Place).
On side A, a user sets the document title to "Q3 Plan". On side B, a different user sets it to "Q3 Planning". The link comes back. Both replicas now hold a value, both values are the result of a successful write that returned 200 to somebody, and there is no fact anywhere in the universe about which one is "later" in any sense that matters — see Happens-Before: The Only Ordering You Actually Have for why physical time does not settle this.
The system cannot refuse to answer. A read is coming, and it must return something. So the question is not *whether* to resolve, only who decides and on what basis — and the default answer in most systems is "a timestamp comparison you did not choose, in a library you did not read".
Where conflicts come from, and why you may already have them
Conflicts are usually associated with exotic deployments, but the generating conditions are mundane. Anywhere two writers can accept a write to the same logical entity without first agreeing, a conflict is possible.
Two of these deserve special mention because teams routinely believe they are exempt. Retries create conflicts: a client times out, retries, and the original write lands afterwards — two writes for one intent (A Timeout Tells You Nothing About Whether It Happened, The Retry Is a Decision, Not a Reflex). And offline clients create conflicts by design: a mobile app that works on a plane *is* a replica that accepts writes during a partition, and the partition lasts six hours.
The single-leader case is genuinely exempt while the leader is stable, because the leader's program order totally orders every write (Leader-Based Replication: Buying Order With a Single Writer). It stops being exempt during a failover, when two nodes may briefly both believe they lead (Split-Brain: Two Nodes, Both Certain They Are In Charge) — which is why fencing exists (Fencing Tokens: Making the Stale Actor Safe, Not Just Unlikely).
- Multi-leader replication — two leaders accept writes by design (Multi-Leader Replication: Accepting Writes in More Than One Place).
- Leaderless / quorum stores — any replica takes a write; concurrent writes to overlapping quorums conflict (Leaderless Replication: Every Replica Accepts Writes).
- Multi-region active-active — the partition is a WAN link, and it will fail (Active-Active: Every Conflict Scenario Becomes Real).
- Offline-first clients — the client is a replica; the partition is "the user was on a train".
- Failover windows — a demoted leader that has not learned it was demoted still accepts writes (Split-Brain: Two Nodes, Both Certain They Are In Charge).
- Client retries — the same intent applied twice, or an old attempt landing after a newer one (The Retry Is a Decision, Not a Reflex).
- Concurrent editors on one document — the classic, and the reason collaborative editors have a whole discipline of their own.
Detect, then resolve — two separate problems
It is worth separating the two questions, because systems conflate them and the conflation is where data disappears.
Detection asks: are these two versions concurrent, or does one descend from the other? This is answerable mechanically from causal metadata — Version Vectors: Making the Conflict Visible give an exact answer, and Vector Clocks: Buying Concurrency Detection at O(N) explain why the metadata must be a vector rather than a number. Without that metadata detection is *impossible*, and a system with no metadata cannot know it is losing anything.
Resolution asks: given that they are concurrent, what value should the system hold? This is not answerable mechanically, because the answer depends on what the data means. Two concurrent additions to a shopping cart should probably both survive. Two concurrent titles for a document probably should not both survive, and which one wins is a product decision. A conflict between "balance = 90" and "balance = 80" cannot be resolved by picking either — the correct answer is 70, and only the application knows that.
The failure mode that costs the most is a system that *resolves without detecting*: it compares two timestamps, keeps the larger, and reports success. It never knew there was a conflict, so it never told you, so nobody found out until a user did. That is Last Write Wins Is Data Loss You Chose by Default, and it is the next lesson because it is the default nearly everywhere.
| Detects conflicts? | Who decides | What it costs you | |
|---|---|---|---|
| Last write winstypical | No | Whichever clock is fastest | Silent loss of the concurrent write |
| Keep siblings, resolve on readtypical | Yes | The reader (often the user) | Complexity in every read path; sibling growth |
| Application merge functionassumption | Yes | You, per data type | Design work, and a rule that must be commutative |
| CRDTprotocol | Not needed — merge is defined | The data type | Metadata, and semantics you may not want |
| Prevent by coordinationprotocol | No conflicts exist | A single writer or consensus | Availability under partition |
The choice you are actually making
Every option above is a position on the same trade. You can prevent conflicts by ensuring only one writer can act — a single leader, a partition-per-entity, a lock — and pay for it with unavailability whenever that writer is unreachable (CAP: What the Theorem Actually Says). Or you can allow conflicts and stay available, and pay for it with the work of resolving them.
There is no third option where you get both, and there is no library that gives you one. What libraries give you is a *default* for the second path, and the default is almost always the cheapest possible resolution rule rather than the most correct one.
The practical discipline: for each piece of replicated state, write down (1) can two writers touch it concurrently, (2) if so, how do I detect that, and (3) what is the right answer when they do. If you cannot answer (3), that is a signal the data needs a single writer, not that you need a cleverer merge.
Key points
- A write conflict is the absence of an ordering, not a bug — the topology creates it, so no amount of correct code prevents it.
- The system must return something, so a resolution rule always exists; the only question is whether you chose it.
- Detection (are these concurrent?) and resolution (what should the value be?) are different problems. Detection is mechanical; resolution is application knowledge.
- Without causal metadata, detection is impossible, and a system that cannot detect a conflict cannot report the data it discards.
- Retries and offline clients generate conflicts in deployments people believe are conflict-free.
- Preventing conflicts costs availability under partition. Allowing them costs resolution work. There is no third choice.
The chain, answered
Every field here is required, which is why no lesson in this domain can recommend a design without naming what an operator sees when it fails, what survives the partition, what repairs it afterwards, and the simpler thing to consider first.
- • Two replicas independently accept and acknowledge writes to the same key while unable to communicate.
- • Each records the write with causal metadata describing what it had seen at the time.
- • Replication or anti-entropy eventually brings the two versions together at some replica (Anti-Entropy: Repairing Divergence Nobody Reported).
- • The receiving replica compares metadata: one version dominating means supersession; mutual non-domination means a conflict.
- • A resolution rule is applied — chosen by you, or defaulted by the store — and the result is replicated onward.
- • The metadata needed to detect the conflict is absent or stripped, so the conflict is resolved as a supersession.
- • The resolution rule is not commutative, so two replicas resolving the same pair in different orders reach different answers and never converge.
- • Siblings accumulate because nothing ever resolves them, and reads grow without bound.
- • A resolution writes back a merged value that itself conflicts with a third concurrent write still in flight.
- • Anti-entropy never runs for a cold key, so two replicas disagree indefinitely and nobody notices.
- • A user's edit silently disappears: both writes returned success, one value survives, and no log line anywhere records that a choice was made. The operator sees a support ticket and no error at all.
- • Two replicas permanently disagree on a key: reads return different values depending on which replica served them, and refreshing the page changes the answer. The operator sees an intermittent, unreproducible bug tied to load-balancer routing.
- • Sibling explosion: reads for a hot key return an ever-growing set of concurrent versions and p99 read size climbs steadily while read *rate* is flat — the tell that resolution is never happening.
- • Divergence after a merge deploy: a non-commutative resolution rule ships, and replicas that previously agreed begin to differ. The operator observes a rising count of anti-entropy repairs that never converge to zero.
- • A cross-region conflict storm after a WAN partition heals: a burst of conflicting versions arrives at once, resolution work spikes, and write latency rises in both regions simultaneously.
- • Detecting and resolving a conflict requires no coordination — that is the entire reason this path exists, and why it stays available under partition.
- • Preventing a conflict requires coordination before every write: a single writer per key, a lock, or consensus (Coordination Couples Availability, Distributed Locks: What They Are Actually For).
- • A middle path is to coordinate only where an invariant spans the writes, and allow conflicts everywhere else (Coordination Avoidance: Restructuring the Problem Instead of Paying for It, Start From the Invariant, Not From the Architecture).
- • During the partition, both sides remain available and both sides remain internally consistent. Neither is wrong.
- • The number of conflicts scales with partition duration times write rate on the contended keys — long partitions are qualitatively worse, not just quantitatively.
- • Any invariant spanning the two writes is unenforced for the duration and may be violated in a way no later merge can repair.
- • Once healed, convergence is guaranteed only if the resolution rule is deterministic and commutative (What "Eventually Converges" Actually Requires).
- • Detect: count conflicts explicitly as a metric, per key range. A system that reports zero conflicts is usually one that cannot detect them.
- • Contain: keep both versions rather than discarding one, so the decision can be deferred to something that knows more.
- • Recover: resolve with an application rule, and write back the merged version so the metadata collapses (Only the Application Knows What the Merge Means).
- • Reconcile: for invariants violated during the partition, run an explicit reconciliation pass — the merge cannot restore an invariant it never knew about (Reconciliation Is a Component, Not a Cleanup Script).
- • Verify: after healing, compare replica state hashes for the affected key range and confirm they agree (Anti-Entropy: Repairing Divergence Nobody Reported, Merkle Trees: Finding the Difference Without Reading the Data).
- • Conflict rate per key range, as an explicit counter. This is the metric most systems do not emit, and its absence is why losses go unnoticed.
- • Sibling count per read at p99, which measures unresolved conflicts accumulating.
- • Replica divergence: periodic comparison of key-range digests, with the number of keys differing after anti-entropy completes.
- • Time from conflict creation to resolution — the window during which reads may return an arbitrary side.
- • Write acknowledgement counts per replica during a partition, which predicts how large the post-heal conflict burst will be.
- • Reasoning about conflicts explicitly is essential for any multi-writer replicated data: carts, preferences, collaborative documents, offline-capable apps, active-active regions.
- • It is the argument that turns "our database handles that" into a specific question about which rule the database applies.
- • It is how you discover that a piece of state needs a single writer — the inability to state a correct merge is the signal.
- • For single-writer state under a stable leader, conflict machinery is pure overhead and complicates every read path.
- • For immutable data, there are no conflicts by construction and no machinery is needed — which is a strong argument for immutability where it fits (The Log Is Not a Queue).
- • Building general conflict resolution for data that could simply be partitioned per user is complexity bought for nothing.
- • Prevent conflicts: route all writes for a key to one owner (Hash Partitioning and the Modulo Trap, Leader-Based Replication: Buying Order With a Single Writer). Cheapest to reason about, costs availability.
- • Make writes commute so the order is irrelevant and no resolution is needed (CRDTs: Deterministic Merge, Not Correct Merge).
- • Make the data immutable and append-only, resolving at read time by projecting the history (The Log Is Not a Queue; Architecture owns the event-sourcing pattern itself, linked below).
- • Detect and refuse: use a version precondition so the second writer gets a conflict error and can retry against fresh state — the optimistic-concurrency approach, which converts a silent loss into a visible failure.
- • Escalate to a human for the small set of conflicts where no automatic rule is correct, which is what good collaborative tools do for genuinely incompatible edits.
Where conflicts come from, and who decides the outcome
| Detects conflicts? | Who decides | What it costs you | |
|---|---|---|---|
| Last write winstypical | No | Whichever clock is fastest | Silent loss of the concurrent write |
| Keep siblings, resolve on readtypical | Yes | The reader, often the user | Complexity in every read path, and sibling growth |
| Application merge functionassumption | Yes | You, per data type | Design work, and a rule that must be commutative |
| CRDTprotocol | Not needed — merge is defined | The data type | Metadata, and semantics you may not want |
| Prevent by coordinationprotocol | No conflicts exist | A single writer or consensus | Availability under partition |
What people believe, and what is true
We use a single database, so we do not have write conflicts.
A single leader prevents them while it is stable. Failover windows, retries and any read-modify-write from two clients reintroduce the same shape.
The database resolves conflicts for us.
It applies *a* rule. Usually the cheapest one. Find out which, because the rule decides whose data is discarded.
Conflicts are rare, so a simple rule is fine.
They cluster: partitions produce a burst of them at once, on exactly the hot keys where the data matters most.
Better replication would avoid conflicts.
Conflicts come from accepting writes at more than one place. The only way replication avoids them is by refusing writes, which is the availability cost you were trying not to pay.
Go deeper
Only the levels this lesson can honestly fill — a missing level is a claim nobody had.
Overview
Two replicas take writes during a partition, so two values exist with no order between them. Something must decide which survives, and if you did not pick the rule, a default did.
Practical
For each replicated entity, write down whether concurrent writes are possible, how you detect them, and what the right answer is. Emit a conflict counter. Prefer keeping both versions over discarding one, and resolve with a rule that knows what the data means.
Advanced
Detection is mechanical given a faithful encoding of the causal partial order; resolution is not, because it depends on semantics no metadata carries. Convergence requires the resolution rule to be deterministic, commutative and associative — a join on a semilattice — which is precisely the property CRDTs formalise and ad-hoc merge functions usually violate.
Apply it
- 🔧 List every replicated entity in your system and mark which can take concurrent writes. For each, name the resolution rule currently in force.
- 🔧 Add a conflict counter to one write path and run a partition test. Compare the count to what your dashboard previously suggested (probably zero).
- ⚡ A user reports that a setting they changed "reverts by itself" occasionally. Two app regions are active-active. How do you confirm the diagnosis?
- ⚡ After a 40-minute WAN partition, write latency spikes in both regions once the link recovers. Explain the mechanism.
- 💬 A partition heals and two replicas hold different values for one key. Walk me through what happens next in a system you have worked on.
- 💬 Your team says "we have a single Postgres, so no conflicts". Where are they still exposed?
- 💬 How would you detect that your system is silently discarding concurrent writes today?