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.
Table of Contents
- 1. How synchronization conflicts technically arise
- 2. Last-write-wins: a simple rule with silent data loss
- 3. Field-level merge strategies instead of whole records
- 4. Conflict detection with version vectors and change counters
- 5. Operational transformation and CRDTs in a mobile context
- 6. User-driven conflict resolution: when automation isn't enough
- 7. Practical sync engines: WatermelonDB, RxDB, and Automerge compared
- 8. Testing conflict scenarios deliberately instead of hoping
- 9. Limits of automatic resolution in practice
- 10. Summary
- 11. FAQ
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.