Chapter 6 — Concurrent Register Writes and Deterministic Convergence
Chapter objective: After this chapter, a developer can explain deterministic convergence of concurrent candidates and verify whether a custom resolver is commutative.
Learning objectives
- Distinguish wall-clock time, HLC, and lineage total order.
- Explain the deterministic basis of the default last-write-wins policy.
- Write a commutativity check for a resolver.
- Identify external effects that equal final values cannot repair.
Prerequisites
- The Register's local view and two-node propagation are understood.
- Each side may accept local writes during a network partition.
Case progress
Node A points fulfillment-primary east while node B
points it west. After recovery, both sides need the same visible hint or
traffic remains split indefinitely.
Determinism matters more than speed
The default ConflictResolver.lastWriteWins() compares
metadata through RegisterLineageOrder. The order includes
lineage/HLC information and uses a stable node identifier as a
tie-breaker. Correctness depends on every node applying the same total
order to the same pair of candidates; one machine's interpretation of
local time cannot supply that guarantee.
At minimum, the following property holds:
resolve(A, B).winner == resolve(B, A).winner
A first-argument-wins resolver can leave A on east and B on west. Duplicate delivery and reversed message order do not heal the split.
The safe entry point for a custom resolver
ConflictResolver.verified(delegate) invokes the resolver
with (local, remote) and (remote, local), then
compares the winning entity and outcome family. A violation raises
CONFLICT_RESOLVER_NOT_COMMUTATIVE, which is useful in
tests, staging, or a controlled rollout.
The check covers only the observed input pair and executes the resolver twice. It is not a formal proof. A resolver remains pure, deterministic, and free of external side effects.
Counterexample and fault injection
Define resolve(local, remote) -> localWins(local).
Each node treats its own candidate as local and the state remains split.
A resolver that reads the current time or a random number can
occasionally agree after argument reversal while still failing
deterministic replay.
Even when two concurrent inventory deductions eventually choose one winner, both customers might already have received success. Conflict resolution converges replica state; it does not undo external commitments.
Experiment
Run the two-node custom-resolver test:
cd submodule/dsm
mvn -q -pl dsm-integration-test -am \
-Dtest=TwoNodeIntegrationTest#customConflictResolversConvergeMergedRegisterValuesAcrossTwoNodes \
-Dsurefire.failIfNoSpecifiedTests=false testThen inspect this chapter's resolver-cases.json and
classify each candidate policy for purity, commutativity, and
determinism.
Experiment acceptance card
| Field | Content |
|---|---|
| Command | The Maven command above;
node --test tests/chapter-assets.test.mjs |
| Input or fault | Concurrent candidates for one key; reversed input order |
| Observable result | Both nodes reach the same merged result; the asset marks unsafe strategies |
| Evidence level | E3: controlled two-node conflict test |
| This experiment does not prove | Formal correctness over every input, cross-resource transactions, or reversal of external effects |
Review
- Concurrent writes require one total order or a commutative merge rule on every node.
- HLC supplies logical ordering, not a globally synchronized clock.
- A custom resolver remains pure, deterministic, and commutative.
- Convergence repairs replicas, not business effects that already escaped.
Next
Chapter 7 turns to time-bounded ownership: shard processing asks who can act within a lease window rather than which candidate is newer.