Mobile Development

Building Offline-First Mobile Apps: Architecture, Sync, and Conflict Resolution

Offline-first apps treat the network as optional, not required. Here is the architecture, the sync strategies, and the conflict resolution that make a Flutter or React Native app work anywhere.

T
The Codememory Team
Codememory
Mar 11, 2026 4 min read
Building Offline-First Mobile Apps: Architecture, Sync, and Conflict Resolution

A mobile app that freezes the moment the signal drops is a liability for anyone working in a warehouse, a delivery van, a remote site, or simply a building with bad reception. Offline-first design fixes this by flipping the default: the network becomes an enhancement, not a requirement. This guide covers how to build offline-first apps in Flutter or React Native — the architecture, the sync strategies, and the conflict resolution that keeps data correct.

What offline-first really means

Offline-first means the app reads and writes to a local database first, and the UI is always driven by that local data. The network is used to synchronise in the background, but the app never waits on it to function. The benefits compound:

  • It always works. Users keep capturing data with no connection at all.
  • It is fast. Reads and writes hit a local store, so the UI never spins waiting on a request.
  • It is resilient. Flaky connections, tunnels, and dead zones become non-events.

The cost is that you now own a distributed-data problem: the same record can change in two places before they reconcile. That is solvable, but it has to be designed in, not bolted on.

The architecture

The shape is the same in both Flutter and React Native; only the libraries differ.

        UI
         │  (reads/writes, always local)
   Local database  ← single source of truth for the UI
         │
   Change queue (pending local mutations)
         │  (background, when online)
    Sync engine  ←→  Backend API / database

Three pieces do the work:

  • A local database as the source of truth for the UI — SQLite, Isar, Drift, or Realm in Flutter; SQLite, WatermelonDB, or Realm in React Native.
  • A change queue that records every local mutation so nothing is lost while offline.
  • A sync engine that runs in the background to push queued changes up and pull remote changes down whenever connectivity returns.

The golden rule: the UI talks only to the local database. It never blocks on the network. Sync happens out of band and updates the local store, which the UI observes reactively.

Sync strategies

How you move data between device and server shapes everything else.

Push and pull

The simplest durable model is a two-way sync built on change tracking. Each side knows what changed since the last successful sync:

  • Push the queued local mutations to the server.
  • Pull records changed remotely since the last sync token.
  • Advance the sync token only after both halves succeed, so an interrupted sync resumes cleanly.

Delta sync over full sync

Never re-download everything on each sync. Use a cursor — a timestamp or server-issued change token — so each sync transfers only what changed. This keeps sync fast and cheap on mobile data.

Idempotent operations

Mobile networks drop mid-request constantly. Give each queued mutation a stable client-generated ID so the server can recognise a retry and apply it once. Without this, a flaky connection produces duplicate records.

Conflict resolution

The hard part of offline-first is that two devices can edit the same record before they sync. You must decide the rule before conflicts happen, not discover them in production. Common strategies:

  • Last-write-wins. The most recent change wins, decided by a reliable timestamp. Simple and often fine, but it can silently discard a legitimate edit.
  • Field-level merge. Track changes per field so two people editing different fields of the same record both keep their edits. Far friendlier than overwriting the whole record.
  • Domain-specific rules. For data that matters — inventory counts, financial entries — encode real logic. Sometimes the answer is to sum changes, sometimes to keep both and flag for review.

Whatever you choose, keep the metadata to enforce it: a version number or vector, plus reliable timestamps. A note on time: never trust the device clock alone for ordering, since phones drift and time zones differ. Prefer server-assigned ordering or versions for anything that must be authoritative.

Practical concerns

A few things separate a demo from a shippable offline-first app:

  • Show sync state. Users trust the app more when they can see "saved locally", "syncing", and "synced". Silent sync breeds doubt.
  • Cap and prune local storage. Devices are not infinite. Decide what to keep offline and evict the rest.
  • Handle schema changes. The local database has its own migrations. A user who has been offline for weeks may upgrade across several versions at once.
  • Secure local data. Data at rest on the device should be encrypted, especially anything sensitive, since phones get lost.
  • Test the unhappy paths. Airplane mode mid-write, a sync that fails halfway, two devices editing the same row — these are the scenarios that matter, so test them deliberately.

A realistic approach

The teams that ship reliable offline-first apps treat the local database as the source of truth, queue every mutation, sync deltas in the background with idempotent operations, and choose a conflict strategy up front rather than hoping conflicts never occur.

This is the approach we take on mobile work at Codememory, in both Flutter and React Native: a local-first data layer the UI always trusts, background delta sync with retry-safe operations, a deliberate conflict-resolution rule chosen per data type, and clear sync status in the interface — so the app is fast and fully usable whether the signal is strong, weak, or gone entirely.

The bottom line

Offline-first is an architecture decision, not a feature you sprinkle on later. Drive the UI from a local database, queue changes while offline, sync only the deltas with idempotent operations, and pick a conflict-resolution strategy before conflicts happen. Get those four things right and your Flutter or React Native app works everywhere — the warehouse, the van, the dead zone — and the network becomes a convenience rather than a dependency.

Frequently asked questions

Offline-first means the app reads and writes to a local database first and treats the network as an enhancement, not a requirement. The UI is always driven by local data, so it stays fast and fully usable without a connection. Sync runs in the background to push local changes up and pull remote changes down whenever connectivity is available.

The architecture is identical; only the libraries differ. Both use a local database (such as SQLite, Isar, or Realm), a queue of pending changes, and a background sync process. Flutter and React Native both have mature packages for each piece, so the decision is less about the framework and more about getting the local-first data flow and conflict strategy right.

With a deliberate conflict-resolution strategy. Common options are last-write-wins using reliable timestamps, field-level merging so non-overlapping edits both survive, or domain-specific rules for cases that matter. The key is to expect conflicts, decide the rule before they happen, and keep enough metadata — versions and timestamps — to apply that rule consistently.

T
The Codememory Team
Codememory