Mobile

Offline-first mobile: designing sync that does not lose work

Intermittent connectivity is harder to handle than no connectivity. Sync design decides whether the app is trusted in the field.

Key takeaways

  • Partial connectivity, not total loss, is the difficult case.
  • Read and write locally; never block the interface on sync.
  • Idempotent writes with client-generated identifiers prevent duplicates.
  • Preserve both versions on conflict and let a person decide.

Partial connectivity is the hard case

A device with no signal is easy to detect. A device on a connection that resolves DNS, opens a socket and then drops most packets is where naive implementations hang, retry and duplicate.

Design for the partial case explicitly: timeouts on every request, idempotent writes, and a queue that survives the app being killed mid-operation.

Local-first data layer

The application reads and writes locally and the interface never blocks on the network. Synchronisation is a background concern with visible status.

  • Client-generated identifiers so a retried write is recognised as a duplicate.
  • An ordered queue that respects dependencies between operations.
  • Explicit, honest sync status — “3 items waiting” rather than an ambiguous spinner.
  • A defined cap and behaviour when a device has been offline for a long period.

Conflicts are a design problem

Last-write-wins silently discards work someone did, which in field applications destroys trust in the data. The person who spent twenty minutes on a record does not care about merge elegance.

Detect conflicts deterministically, preserve both versions with provenance, and present a clear choice with enough context. Most conflicts are rare enough that a human decision is affordable.

Test the conditions that actually occur

Test with a proxy that drops packets and adds latency, not with wifi switched off. Test the app being killed mid-sync, the device running out of storage, and a token expiring while queued work is pending.

These are the failure modes that produce support tickets, and they are all reproducible if you decide to reproduce them.

Communicating state to the user

Field users trust an application when they can tell what it has done with their work. Ambiguity about whether something was saved is the fastest route to people keeping a paper backup, which defeats the purpose.

Show explicit, honest state: saved locally, queued to send, sent, and failed with a reason. Distinguish between waiting for a connection and having failed permanently, because the user's response differs.

Where something cannot be completed offline, say so at the point the user attempts it rather than accepting the input and failing later. Clear refusal is better than false acceptance.


Frequently asked questions

SQLite through a platform wrapper is the dependable default. Purpose-built sync engines can save time when their model matches yours; check how they handle conflicts before committing.

Meaningfully to the affected features, and more to testing. It is also almost entirely front-loaded retrofitting later changes the data layer and everything above it.

No. Classify each operation as safe offline, safe with reservation, or requiring connectivity. Blocking some operations clearly is better than a conflict flow nobody understands.

Working on this?

A conversation with our team, no charge and no obligation.

Book a conversation