Mobile Architecture

Offline-first Mobile Platform

How a field-services enterprise stopped losing inspections to dead zones.

Lead mobile engineer — architecture, sync design, shared foundations, release engineering.

FlutterAndroidSQLiteREST

The brief

“Every inspection our teams complete in a basement or a parking structure is money on the table until it syncs. We’ve lost forms, we’ve double-billed clients, and our inspectors have learned not to trust the app. We need software that works where they work — and where they work, there’s usually no signal.”

That was the problem, in the client’s words: a field-services and inspections enterprise with hundreds of devices in daily use, whose most valuable work happened precisely where connectivity didn’t.

Zerolost field submissions since rollout
100sof devices syncing across distributed sites
16+subsequent projects built on the shared foundations
98.3%test success rate sustained across the codebase

Context & constraints

The operating reality shaped every technical decision:

  • Connectivity is the exception, not the rule. Inspectors routinely spend full working hours underground, in stairwells, or on remote sites.
  • Zero tolerance for lost field data. A lost inspection form isn’t an inconvenience — it’s rework, a billing dispute, and eroded trust in the tooling.
  • A mixed, budget Android fleet. Storage, memory, and battery budgets were real constraints, not footnotes.
  • One team, many apps to come. The platform had to be a foundation — its architecture and components would seed future field applications, not serve a single product.

The architecture

Three decisions defined the platform.

The platform’s sync architecture: local-first writes, a transactional outbox, and a sync engine that treats connectivity as an occasional visitor rather than a dependency.
The platform’s sync architecture: local-first writes, a transactional outbox, and a sync engine that treats connectivity as an occasional visitor rather than a dependency.

1. Local-first, not cache-first

Most “offline support” is a cache bolted onto a network-first app — usable until the network disappears mid-workflow. This platform inverted the model: the on-device database is the system of record, and the network is a replication concern. Every write commits locally in milliseconds; the UI never waits on a request; a sync engine reconciles with the backend whenever connectivity allows.

Conflict resolution was designed per entity type rather than globally: reference data is server-authoritative, inspection payloads are field-authoritative — the person standing on site knows best — and every reconciliation leaves an audit trail. Client-generated identifiers make server operations idempotent, so the same submission arriving twice can never create a duplicate record.

2. A durable operation queue

Sync has to survive everything a field day throws at it: app kills, dead batteries, reboots at 2% charge. Every mutation is recorded as an operation in a persisted outbox, committed in the same local transaction as the data change — the queue can never disagree with the database. When a connection appears, the queue drains in dependency order with exponential backoff and jitter; failed operations retry safely, and poison operations are parked for inspection instead of blocking everything behind them. The queue lives on disk, not in memory — a crash loses nothing.

3. A media pipeline built for bad networks

Inspection photos are the heaviest payloads and the most likely to fail. The pipeline treats them accordingly: images are compressed and downscaled at capture, stored outside the database with references inside it, and uploaded through chunked, resumable transfers — a dropped connection resumes where it stopped instead of starting over. Sync is prioritized by business value: small, structured inspection data goes first; media trickles behind it. Once safely uploaded, local originals are reclaimed to respect the fleet’s storage budgets.

Components & stack

A single Flutter/Dart codebase for the Android-first fleet · SQLite-backed local store · background workers scheduled through the platform’s native work managers · REST APIs with idempotency keys · a layered architecture (UI → application services → repositories → sync engine → transport) in which the sync engine is fully isolated from feature code — features write locally and never know sync exists.

Outcomes

  • Zero lost field submissions since rollout. The failure mode the client described in the brief simply stopped occurring.
  • Sync-related support tickets fell to near zero within two release cycles — and with them, the billing disputes they used to cause.
  • The platform became the baseline. Its shared foundations seeded a component library reused across 16+ subsequent projects; new field applications now bootstrap in days rather than weeks.
  • Reliability is enforced, not hoped for. The sync engine is covered by deterministic tests that simulate connectivity loss mid-operation, part of an engineering discipline sustaining a 98.3% test success rate.

Retrospective

Two things would be done differently today. Conflict visibility shipped later than it should have — early versions resolved conflicts silently, which was correct but occasionally surprising to the people whose data was merged; “what changed and why” belongs in the first release, not a later one. And sync observability — per-device queue depth and failure dashboards — would be built before rollout rather than after the first support escalations. Both are now defaults in every platform that followed.

The patterns this platform proved — local-first storage, a transactional outbox, resumable media — are no longer a project. They’re the baseline every field application after it was built on.