Offline Sync Conflict Resolution: Strategies for Mobile Apps
AI generated
RN
native
React Native / Data Management
Offline Sync Conflict Resolution
Strategies for mobile apps running on multiple devices offline at the same time

As soon as two devices edit the same record offline and both later synchronize, a conflict inevitably arises that the app has to resolve. This article shows how such conflicts technically occur, which resolution strategies hold up in practice, and where automatic solutions reach their limits.

11 min read Conflict Resolution Sync Strategies

1. How synchronization conflicts technically arise

A synchronization conflict occurs whenever two devices modify the same record offline and both changes are later pushed against the same server state. At the time the first incoming change arrives, the server only knows one version, but shortly afterward sees a second request that starts from that exact same baseline, even though the record has already been modified in between.

Mobile apps hit this scenario far more often than pure web applications, because airplane mode, subway rides, and patchy coverage create longer offline windows. Two field sales reps editing the same customer record at the same time on their own devices almost inevitably produce conflicting changes that only surface at the next successful sync.

2. Last-write-wins: a simple rule with silent data loss

Last-write-wins is the simplest conflict resolution approach: the change with the higher timestamp takes precedence, and the other one gets silently discarded. The rule can be implemented in a few lines of code and works well enough for low-stakes values like a last opened folder or a sort preference.

Last-write-wins turns problematic once device clocks drift out of sync or a device was offline and only corrects its clock later. In that case, the change that actually happened last does not win, the change with the coincidentally higher client timestamp does, which can silently drop business-critical data without anyone noticing.


// Naive last-write-wins implementation in the sync layer
type Record = { id: string; updatedAt: number; payload: Record<string, unknown> };

function resolveLastWriteWins(local: Record, remote: Record): Record {
  // Warning: relies entirely on the client clock,
  // clock drift between devices is not detected here.
  return local.updatedAt >= remote.updatedAt ? local : remote;
}

3. Field-level merge strategies instead of whole records

Instead of overwriting an entire record, a field-level merge compares every single value against its own change timestamp. If one device changes only a contact's phone number and another device changes only the note at the same time, both changes survive because they simply do not overlap.

This strategy meaningfully reduces the number of real conflicts, because in most forms the majority of fields are edited independently. The cost is extra storage overhead, since each field needs its own timestamp and sometimes its own revision counter, rather than a single column covering the whole record.


// Field-level merge: only genuinely colliding fields get checked
type FieldMeta = { value: unknown; changedAt: number };
type Entity = Record<string, FieldMeta>;

function mergeFields(local: Entity, remote: Entity): Entity {
  const result: Entity = { ...local };
  for (const key of Object.keys(remote)) {
    const remoteField = remote[key];
    const localField = local[key];
    if (!localField || remoteField.changedAt > localField.changedAt) {
      result[key] = remoteField;
    }
  }
  return result;
}

4. Conflict detection with version vectors and change counters

Plain timestamps only tell you which change happened later, not whether two changes actually happened concurrently and independently of each other. Version vectors solve that problem by having each device keep its own counter per record and increment it on every local change.

When the server compares two version vectors, it can reliably tell whether one change builds linearly on the other or whether both arose independently, meaning concurrently. Only in the second case is there a genuine conflict that needs a resolution strategy, while a purely sequential chain of changes can be adopted automatically without risk.

5. Operational transformation and CRDTs in a mobile context

Operational transformation originates from collaborative text editors and transforms incoming operations so they converge to the same result regardless of order. For typical mobile CRUD apps with forms, this approach is usually overkill, since it requires central transformation logic that must be correctly defined for every possible field combination.

Conflict-free replicated data types, or CRDTs, instead mathematically guarantee that multiple replicas converge to the same result without central coordination. Libraries such as Automerge or Yjs work well for counters, sets, or collaborative text, but bring noticeably larger metadata and make debugging simple business records considerably harder.

6. User-driven conflict resolution: when automation isn't enough

Some conflicts should not be decided automatically, for example when two sales reps stored different discounts for the same order while offline. In such cases the app should surface the conflict, show both versions side by side, and let the user make the active decision instead of silently discarding one of them.

In practice, a dedicated conflict queue works well, holding back affected records until the user makes a decision. Only after that decision does the record finally get synced, while every other conflict-free change keeps processing automatically in the background.


function ConflictBanner({ local, remote, onResolve }: ConflictProps) {
  return (
    <View style={styles.banner}>
      <Text style={styles.title}>Conflicting change detected</Text>
      <Text>Local: {local.discount}% discount</Text>
      <Text>Server: {remote.discount}% discount</Text>
      <View style={styles.actions}>
        <Button title="Keep local version" onPress={() => onResolve(local)} />
        <Button title="Use server version" onPress={() => onResolve(remote)} />
      </View>
    </View>
  );
}

7. Practical sync engines: WatermelonDB, RxDB, and Automerge compared

WatermelonDB follows the pullChanges and pushChanges pattern and ships with a simple last-write-wins default, but lets you hook a custom resolver between the pull and push steps. RxDB works with a revision chain per document and detects a conflict as soon as the local revision no longer matches the expected server revision.

Automerge takes a fundamentally different path and merges changes automatically as a CRDT, without any explicit conflict step, which works well for structured documents like notes or configuration. For classic relational business data with foreign keys, a combination of field-level merge and a narrow manual escalation layer usually remains the more robust choice in practice.

8. Testing conflict scenarios deliberately instead of hoping

Conflict resolution cannot be reliably validated through manual trial and error, because the interplay of network timing and local state is barely reproducible by hand. A far better approach is an integration test where two simulated clients modify the same baseline record offline and then send both changes sequentially against the same server endpoint.

It also pays off to run targeted chaos testing with random offline windows and delayed responses, for example through a mocked NetInfo implementation in React Native. On the server side, an idempotency check belongs in the mix too, catching duplicate incoming changes so a repeated sync request after a dropped connection doesn't accidentally create another conflict.

9. Limits of automatic resolution in practice

For stock levels, prices, or payment status, automatic merging is risky, because an incorrectly merged value can have direct financial consequences. For such fields, automation should deliberately escalate instead of making a plausible-sounding but potentially wrong assumption and hiding the conflict opaquely in the background.

A hybrid approach works well in practice: automatic resolution for low-risk fields like notes or view preferences, a manual review queue for business-critical fields, and a consistent audit log that documents every conflict decision in a traceable way and can serve as evidence in a dispute.

Strategy How it works Suited for Risk
Last-write-wins Higher timestamp wins, the losing change gets discarded Low-risk single values like view preferences Silent data loss under clock drift
Field-level merge Each field gets its own timestamp, independent fields survive Forms with several independent fields Higher storage and comparison overhead per field
CRDT-based types Mathematically guaranteed convergence without a central authority Collaborative text, counters, sets Larger metadata, harder debugging
Version vectors Detects genuine conflicts instead of relying on plain time ordering Distributed systems with multiple writers Additional server logic to evaluate
User-driven resolution User gets a diff view and decides actively Business-critical fields like prices or quantities Interrupts the workflow, requires UI work

Mironsoft

React Native app development and Magento integration

A mobile app for the Magento shop that actually runs smoothly?

We build React Native apps cleanly connected to the Magento REST or GraphQL API, from the first line of code to publishing on the App Store and Google Play.

App Concept

Plan the architecture and feature scope of a Magento-connected app together.

Magento API Integration

Cleanly connect product catalog, cart, and checkout to the shop API.

Store Publishing

Guide the App Store and Google Play release process without pitfalls.

10. Summary

Offline Sync Conflict Resolution: Key Takeaways

Conflicts are unavoidable

As soon as multiple devices edit the same record offline, synchronization inevitably produces a conflict that the app has to actively handle.

Last-write-wins rarely suffices

Plain timestamp comparisons cause silent data loss under clock drift, especially for business-critical fields.

Field-level merge cuts collisions

Handling conflicts per field instead of per record loses independent changes far less often.

Automation has limits

At high risk, the app should escalate and let the user decide actively instead of guessing.

11. FAQ: Offline Sync Conflict Resolution: Key Takeaways

1What actually causes sync conflicts in offline-first apps?
Conflicts arise when two devices modify the same record from the same baseline version offline, and both changes are later synchronized against the same server state.
2Why is last-write-wins risky?
Because the rule relies on client timestamps, which don't always reflect the actual order of changes due to clock drift or delayed synchronization, which can cause silent data loss.
3What's the difference between last-write-wins and field-level merge?
Last-write-wins decides for the entire record, while field-level merge compares each field individually and keeps independent changes from both versions instead of discarding one wholesale.
4When are CRDTs worth using in a React Native app?
Mainly for collaborative content like jointly edited notes or counters, where mathematically guaranteed convergence matters more than a small metadata overhead.
5How does a system detect a real conflict instead of just a later change?
Through version vectors, where each device keeps its own counter per record. Only when two vectors sit side by side instead of building on each other linearly is it a genuine conflict.
6When should the app actively ask the user for resolution?
Whenever an automatic decision could have financial or business-critical consequences, for example with prices, discounts, or stock levels.
7Does WatermelonDB support conflict resolution out of the box?
WatermelonDB ships a simple last-write-wins logic within its pullChanges/pushChanges cycle, but lets you hook in a custom resolver for more specific requirements.
8How do you reliably test conflict scenarios?
Best done with integration tests where two simulated clients modify the same baseline record offline and then send both changes sequentially against the same server endpoint.
9What happens if conflicts aren't handled at all?
Without explicit handling, the most recently arriving request typically overwrites the previous one silently, leading to unnoticed data loss and inconsistent state across devices.
10Is an audit log worth keeping alongside conflict resolution?
Yes, a consistent audit log documents every conflict decision in a traceable way and serves as concrete evidence of what was decided, when, and why, in a dispute or support request.