The field rep drove into a dead zone. The mobile app kept working: they filled out three forms, updated two account records, closed a deal. Forty minutes later, connectivity returned and the sync ran. Two of those records had been updated by a desktop user in the meantime. The mobile changes were silently dropped. No error. No prompt. Just gone.

The Core Problem#

The client operates against a local snapshot while offline. The server keeps evolving. On reconnect, two diverged histories from a common base version. The question is how to reconcile them without losing data.

Three Strategies#

Last-write-wins is the simplest and the most lossy. Fine for low-stakes data like “last seen” timestamps. Dangerous for anything users put effort into. The losing write is silent. Field-level merge compares each field independently against the base version snapshot. Non-conflicting field changes merge automatically. Conflicting field changes surface to the user for resolution. This is essentially Reconciliation applied at the field level. Op log replay stores every operation the offline client performed (entity ID, field, op type) and replays on current server state on reconnect. This requires every op to be idempotent, otherwise replaying a partially-applied log causes double effects. Idempotency is not optional here.

Version Counters#

The server stamps every record with a version counter. The client records the version it last synced from (the “base version”). On sync, the client sends local changes plus base version. If server version is greater than base version, the changes are based on stale data: present merge UI, do not silently overwrite.

graph TD Base["BaseVersion (v3)"] --> Local["LocalChanges (mobile: status=closed, amount=50k)"] Base --> Server["ServerChanges (desktop: status=open, notes=added)"] Local --> Merge["MergeStep"] Server --> Merge Merge --> Conflict["Conflict: status changed on both sides (surface to user)"] Merge --> AutoMerge["AutoMerge: amount from local, notes from server"] Conflict --> Resolved["ResolvedRecord (v4)"] AutoMerge --> Resolved style Base fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff style Local fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff style Server fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff style Merge fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff style Conflict fill:#000000,stroke:#ff0000,stroke-width:2px,color:#fff style AutoMerge fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff style Resolved fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff

At Salesforce, mobile app users in poor-connectivity areas filled out forms offline. On reconnect, sync failed silently when the server record had been updated by a desktop user during the offline window. No conflict surfaced, mobile data dropped. After three customer complaints about lost field data, we added a version counter to every record. Mobile client now sends its base version on sync. If server version exceeds base version: merge UI instead of silent overwrite. Silent data-loss complaints dropped to zero.

What I’m Learning#

Version counters are the kind of thing you regret not having from day one. Once records are in production without them, retrofitting is a migration project that always feels lower priority than it is.

Have you built offline sync into a mobile or desktop app? Did you go with last-write-wins or something more sophisticated?