Skip to content
Mobile Architecture

Offline-First in Flutter: The Sync Strategy I Actually Use

How I handle data conflicts and sync in the Ventri Farm app — which must keep working in barns without signal.

Wafik Ulinnuha

Backend Developer

2 min read

The Ventri Farm app runs in chicken barns, far from cell towers. Signal drops every few minutes. A UX that shows a loader on disconnect is the same as unusable. The solution is offline-first design.

The principles I hold to

  1. Write locally first. Every user action becomes a record in SQLite with local_id, created_at, and a sync_status flag.
  2. The server is the ultimate source of truth. The local store is an authoritative cache only until sync succeeds.
  3. Conflicts resolve deterministically. No "choose a version" dialog for field operators.

The local schema

I use drift (formerly Moor) in Flutter. Every domain table has a shadow table for the sync queue:

class FieldEntries extends Table {
  TextColumn get localId => text()();
  TextColumn get serverId => text().nullable()();
  IntColumn get version => integer().withDefault(const Constant(0))();
  TextColumn get payload => text()(); // JSON
  IntColumn get syncStatus => intEnum<SyncStatus>()();
  DateTimeColumn get updatedAt => dateTime()();

  @override
  Set<Column> get primaryKey => {localId};
}

Conflict resolution strategy

For Ventri, most entities are append-only (event records). True conflicts are rare. For mutable entities (chicken master data), I use Last-Write-Wins backed by a simple vector clock: the server stores a version that bumps on every update, and clients must send If-Match with the last known version. The server rejects updates if its version is newer, and the client is asked to re-merge.

Background sync

I do not force sync on the main isolate. A separate worker runs every 30 seconds (foreground) or fires via workmanager every 15 minutes (background). The queue is FIFO but I batch up to 50 records per request to keep payloads sane the moment 4G returns.

What I would do differently

  • Adopt CRDTs (like Automerge) from day one for collaborative entities. For single-operator-per-record, plain LWW is fine.
  • Be more aggressive about sync metrics: how long do records sit in the queue? What is the conflict ratio? Without numbers, optimization is guesswork.

Offline-first is not an add-on feature. It changes how you think about your domain model. The investment is real — but so is the return: an app that does not blame the operator when the WiFi disappears.

Topics

#Mobile #Architecture

Share

Back to all posts

Further reading

Further reading