A product that runs on more than one device eventually has to answer a question nobody likes putting a number on: what happens when the same record changes in two places before either device has seen the other’s update. Calendar apps, family organisers, personal finance trackers, rota planners — anything a user opens on a phone and then a laptop an hour later — run into this in the first few weeks of real use, not at some future scaling milestone.
It is worth treating as a design decision rather than a bug to fix later. The three common approaches — last-write-wins, field-level merging, and CRDTs — trade off implementation cost, storage overhead and how forgiving they are of mistakes. Picking the wrong one does not usually break the app outright; it produces the quiet kind of bug where a user’s edit disappears and nobody can say why.
Why this shows up earlier than teams expect
A server-authoritative CRUD app mostly avoids this problem: one client talks to one database, and the database resolves ordering. The moment a product needs to work offline, or simply needs to feel instant on a phone before a round trip to the server confirms anything, each device starts holding its own copy of the data. Two copies that can both change without checking in with each other is the entire definition of a conflict. It does not take a large team of collaborators to trigger it — a single user with a phone and a laptop open at the same time is enough.
Three ways to resolve a conflict
Last-write-wins
Attach a timestamp or version number to a record and let the most recent write overwrite the rest. It is cheap to build, easy to reason about, and fine for flat, independent fields — a display name, a notification preference, a single numeric setting. It falls apart on structured or shared data, where the newest write can silently discard a change that mattered. A shift gets reassigned on one device while someone else marks it completed on another; last-write-wins picks one outcome and throws the other away without telling anyone.
Field- or operation-level merging
Instead of replacing a whole record, merge it field by field, or apply operations (append, increment, add-to-list) rather than overwrite whole values. This needs domain knowledge baked into the merge logic — someone has to decide, for every field, what “correct” looks like when two versions disagree. It works well when conflicts are genuinely rare and the merge rule is obvious: a running total that only ever increases, a list that only ever gains items, a status field with a defined precedence order.
CRDTs
Conflict-free replicated data types are structures — for text, lists, maps, counters — that are mathematically guaranteed to converge to the same state on every device, regardless of the order updates arrive in, with no central coordinator needed at the moment of the edit. They handle rich, collaborative data well: shared documents, ordered lists that multiple people reorder, free text edited concurrently. The cost is real: extra metadata stored alongside every value (tombstones for deleted items, per-field version vectors), a library dependency that shapes how the data layer is built, and a genuine learning curve for a team that has not worked with them before. Debugging “why does this list have a duplicate” is a different skill to debugging a REST endpoint.
What actually decides which one fits
- How many devices or people can realistically edit the same record within the same offline window — two devices belonging to one user behave very differently to five family members editing a shared list.
- The shape of the data: flat independent fields tolerate last-write-wins; nested, ordered, or collaboratively-edited structures need merging or CRDTs.
- How long offline periods actually last in practice — a few seconds of network blip is a different problem to a device that goes offline for a day.
- Whether a silently-wrong merge is tolerable or dangerous. A lost to-do item is an annoyance; a financial transaction merged the wrong way is not.
- Whether the team already has the skills to operate the chosen approach, or whether it would be the first CRDT anyone on the team has shipped.
Build the sync layer or adopt one
Sync engines and CRDT libraries exist precisely because this problem is hard to get right from scratch — Automerge and Yjs for CRDT-backed data structures, PowerSync and ElectricSQL for syncing a local SQLite database against a Postgres backend, Replicache for a more application-level sync protocol. Adopting one saves months of edge-case debugging, but it also couples the data model to that library’s assumptions about how records are shaped and how updates propagate. For most products, adopting is the right default. Writing a custom sync layer is worth it only when the data does not fit any available library’s model, or when the conflict semantics are business-critical enough that off-the-shelf defaults are not acceptable — financial ledgers being the clearest example.
Where the stakes differ by product type
The right answer depends heavily on what the data actually is, not on the product category in the abstract. A shared household calendar or family task list has concurrent editing as its normal case, several people, overlapping edits, all day — that is where CRDTs or careful field merging earn their cost. A personal finance tracker is mostly append-only: transactions arrive with unique identifiers and rarely need merging at all, so last-write-wins on the few mutable fields (a category, a note) is often sufficient without any CRDT machinery. Rota and shift-scheduling data sits in between: conflicts are rare but high-stakes, and the safer design is often to detect a double-booking and surface it to a human rather than silently resolve it either way. IoT telemetry buffers usually sidestep the whole question, because each device is the sole writer of its own data — the real problem there is out-of-order delivery, not concurrent edits, and needs a different fix entirely.
Questions to settle before writing any sync code
- How many devices or users can edit the same record inside one realistic offline window?
- Is the data flat and independent, or nested, ordered and collaboratively edited?
- Can a wrong-but-silent merge be tolerated, or does it need to surface to a human instead?
- Does an existing sync library already model this kind of data well, or would adopting one mean working against its assumptions?
- Who owns the sync layer after launch — is there someone on the team who can actually debug a bad merge when one turns up in production?
Answer those before the data model is fixed, not after the first support ticket about a vanished entry. Last-write-wins is not a lesser choice for being simple — it is the right one whenever the data genuinely is flat and low-stakes. The mistake is picking it by default for data that was never flat to begin with.