beginner 3 min answer

Two inspectors both work offline on the same site photo. One adds the tag "damp" and edits the caption; the other adds the tag "crack" and edits the same caption. After sync both tags are present and only one caption survived. The app uses the same CRDT library for both fields. Why did the same merge engine protect one field and silently discard the other?

crdtslww-registerdata-modellingoffline-firstconflict-granularity
Show the full answer Hide the answer

The mechanism

A CRDT library does not merge "the record". It merges each field according to the CRDT type you chose for that field, and the two fields here were given different types.

The tag list is almost certainly an add-wins or observed-remove set. Its merge rule is union: two concurrent adds are two different pieces of information, so both survive. The caption is a last-writer-wins register. Its merge rule is "keep the value with the higher timestamp or replica tie-break, discard the other". Both merges are correct, deterministic and convergent. Only one of them keeps both users' work.

Convergence says every replica ends up with the same value. It says nothing about whether that value contains everyone's contribution. An LWW register converges perfectly while throwing away an arbitrary half of the input, and it does so with no error, no conflict marker and nothing in the logs.

What the data model is actually deciding

Choosing a CRDT type per field is choosing, in advance, what "conflict" means for that field:

  • Register (LWW or multi-value) — the field has one logical value and a later value supersedes an earlier one. A thermostat setpoint, an order status, a courier's availability toggle.
  • Set / map — membership, where each add and remove is independent information. Tags, labels, attendees.
  • Counter — an aggregate of independent increments. Likes, inventory movements.
  • Sequence / text type (RGA, YATA and relatives) — character- or element-level intent, which is what prose needs if two people may type into the same paragraph.

The caption is prose, so the honest options are a text CRDT or a multi-value register that surfaces both versions to the user. Either one costs metadata. A character-addressed text CRDT carries an identifier and causal metadata per character, which is on the order of tens of bytes per character before compaction, so a 2 KB caption can carry 20–60 KB of merge metadata. That is why nobody models every string in an app as a text CRDT, and why a register is a legitimate choice for most short fields.

When the register is the right call

Use a register when the field has one logical writer, or when a later value genuinely replaces an earlier one rather than adding to it. A status, a setpoint, a flag, a chosen option: for these, keeping both values is meaningless and an LWW register is the cheapest correct answer.

Switch to a type that preserves both sides when either side's edit is information the other side did not have. A caption written by two inspectors who each saw something different is exactly that case. The cheap middle path is a multi-value register: keep both, show the user both, and let a human pick. It costs one interface screen and zero merge theory.

The decision rule that follows: model the field by who can write it and whether a later write supersedes, not by its storage type. Two strings in the same table can correctly deserve different merge rules.

Common weak answers

  • "The library has a bug." Both merges did what their types specify. The defect is in the modelling, and no library can guess that a caption is collaborative while a status field is not.
  • "Use a CRDT for everything." That is what happened. The caption is a CRDT, and a CRDT that discards concurrent edits by design.
  • "Add timestamps so the newest edit wins." That is already the behaviour, and the timestamps are the hazard: a device clock that is 40 seconds fast makes an older edit win forever.
  • "Warn the user about the conflict." There is no conflict to warn about. The register resolved it before anything reached the server, on the device, during merge.