Offline-first mobile apps: building for a patchy network
Connectivity across much of the region is intermittent rather than absent. An app that assumes a working connection fails in exactly the moments users need it.
Key takeaways
- Read locally and render immediately, then refresh in the background; never make the user wait on the network to see data.
- The hardest state is connected with no throughput; set aggressive timeouts and fail into the offline path.
- Show pending writes visibly, and surface queue failures rather than losing them silently.
- Conflict resolution is a product decision per data type, not an implementation detail.
An offline-first app treats the local device as the source of truth for reading and queues changes for synchronisation when a connection is available. This matters in markets where connectivity is intermittent rather than reliably absent, because an app that shows a spinner every time the signal drops is unusable exactly when a user most needs it: in a lift, in a basement, on a road, in a building with poor coverage.
The three states most apps get wrong
- Online and healthy. The only state most apps are designed and demonstrated in.
- Offline. No connection at all. The app should still open, show previously loaded data with an honest indication of when it was fetched, and accept input to be sent later.
- Online but useless. Connected to a network with no real throughput, which is the most common and worst-handled state. A request that hangs for thirty seconds is worse than an immediate failure, so set aggressive timeouts and fail into the offline path.
Architecture that works
Read from a local store and render immediately, then refresh from the network in the background and update. The user never waits on the network to see something. Writes go to the local store first, are queued, and are sent when possible, with a visible indicator that something is pending rather than a silent queue the user cannot see.
Design for the queue failing. A change that cannot be sent needs to surface to the user eventually rather than disappearing, and the message should say what has not been saved and what they can do about it.
Conflicts are a product decision
When the same record changes in two places, someone has to decide which wins. Last write wins is simple and silently destroys data. Prompting the user is honest and intrusive. Merging is best where the data structure allows it. This is not a technical detail to be resolved during implementation; it is a product decision with consequences, and it should be made explicitly per data type before building.
What not to store locally
Anything sensitive that does not need to be available offline. A cached copy of financial detail or identity documents on a device is a liability, and the convenience rarely justifies it. Where local storage of sensitive data is genuinely required, use the platform's encrypted storage rather than a general-purpose database, and clear it on sign-out.
Tell the user the truth about freshness
An offline-first app is showing cached data, and the user needs to know how old it is. A quiet timestamp, or a line saying last updated eleven minutes ago, converts a potentially misleading screen into an honest one. The alternative, showing stale numbers as though they were live, is how someone acts on a balance or a stock level that changed an hour ago.
Distinguish between stale and unavailable. Data you have but cannot refresh should still be shown with its age. Data you have never fetched should say so plainly rather than displaying zeros, which read as real values and are the most common source of confused support tickets.
Test the transitions, not just the states
Most offline bugs occur at the boundary: the moment connectivity returns and a queue of changes flushes, or the moment it drops mid-request. Test going offline while a write is in flight, coming back online with several queued changes, and having a queued change rejected by the server because the underlying record changed. Those three scenarios find nearly every serious defect, and none of them appears in a normal test pass on a good connection.
Online but useless is the most common network state and the worst handled. Fail fast into the offline path.