The first version of the Farm MGT Android app worked offline in the same way that most apps claim to work offline — it cached some data locally and gave a vague error message when connectivity was lost. Farm workers hated it. They never knew which features would work without internet and which wouldn't.
The second version was built with a different mental model: assume no internet by default. Every feature had to work without connectivity. Internet became an enhancement for syncing, not a requirement for operation.
The Architecture Decision: SQLite Local, Sync Later
The core change was giving the Android app its own complete SQLite database — a local replica of the farm's data, kept in sync with the server when connectivity is available. Every write goes to local SQLite first. The sync engine pushes changes to the server and pulls server-side updates in the background.
This sounds straightforward. The complexity is in conflict resolution. When a field worker records an animal's weight at 2pm without internet, and a farm manager updates the same animal's health record at 2:30pm via the web dashboard, which version wins when the app syncs at 5pm?
Our resolution strategy: last-write-wins per field, using device-local timestamps with NTP synchronisation on sync. For compound records (vaccination events, weights), we treat each record as an immutable append — no update conflicts, only new records that may need deduplication.
What "Offline" Actually Means in Practice
We did ethnographic observation with farm workers at three farms before writing the second version. The connectivity pattern was more nuanced than we expected:
- Farm homesteads and offices typically had reliable 4G or WiFi
- Paddocks and fields within 2km of the homestead had intermittent 3G/4G (1–3 bars)
- Remote paddocks and fields beyond 3km were frequently zero-connectivity
- The morning milking period (4am–8am) was consistently the lowest-connectivity window, because workers arrive from off-farm and haven't connected to homestead WiFi yet
This meant the app needed to handle not just "completely offline" but also "low connectivity" — where network requests would sometimes succeed and sometimes fail mid-flight.
Handling Partial Connectivity
We implemented a request queue with exponential backoff. Every outbound API call is logged to a local queue first. A background service attempts to drain the queue whenever connectivity is detected. Failed requests stay in the queue and retry with increasing delays. The UI shows a sync status indicator so workers know when their records have been confirmed server-side.
Photo attachments (wound photos, crop disease images) follow a separate lazy-sync path — they sync at lower priority and only over WiFi by default, to avoid eating mobile data.
The UX Rule: Never Block on Network
Every UI action in the app completes immediately from the user's perspective. Tapping "Save vaccination record" writes to SQLite and shows a success state — it does not wait for a server acknowledgement. The sync happens in the background. This means farm workers never see loading spinners caused by network latency, and they can work at their own pace regardless of signal.
The only exception is the initial data download on first login — we require connectivity to pull the farm's animal registry and configuration. After that, the app is fully self-sufficient.
What We'd Do Differently
If we were starting again, we'd invest earlier in a more formal conflict resolution protocol — something closer to CRDTs for the fields that are most commonly edited by multiple users simultaneously (animal status, current location, daily milk totals). The timestamp-based resolution works well in practice but has theoretical edge cases we're still thinking through.
We'd also build the sync engine as a separate library from the start, rather than extracting it from the app codebase at version two. It's now generic enough to be reusable across other Vetra mobile products.