Skip to content
Mobile6 min read

Offline-first: designing for the network you do not have

Offline support added to a finished application is a rewrite. The reason is that connectivity is not a feature that sits beside the others — it determines where the truth lives, and that decision shapes every screen, every write and every piece of state in the product.

Part ofMobile App Development

Mobile networks are not a binary. The difficult state is not "offline", which is at least detectable — it is the lift with one bar, the train entering a tunnel mid-request, the hotel network that resolves DNS and drops everything after. An application that only handles the clean cases fails in exactly the situations people remember.

Four levels of support, chosen deliberately

LevelBehaviourCost
Online onlyBlocks with an error when the network failsLow, and visibly poor on real networks
Graceful degradationShows cached data, blocks writesModerate — a read cache and honest state
Offline readsFull read access to synced data, queued writesSignificant — needs a real local store
Offline-firstLocal store is the source of truth, sync is backgroundHighest, and the only one that feels instant

Most applications should be at level two and are at level one. Level four is genuinely expensive and worth it for field work, logistics, healthcare, retail floor staff and anything used somewhere with unreliable coverage — where the alternative is not a worse experience but an unusable product.

Offline-first means the local store is the truth

The architectural inversion is the whole idea: the user interface reads and writes locally, always, and synchronisation happens separately in the background. Nothing in the interface waits for a server.

text
Server-first — every interaction is a network round trip:

  tap ──▶ spinner ──▶ request ──▶ response ──▶ update UI
          ^^^^^^^ the user waits here, and fails here

Local-first — the network is never in the interaction path:

  tap ──▶ write to local store ──▶ update UI        (instant)
                 │
                 └──▶ enqueue change ──▶ sync when possible
                                              │
                                              └──▶ reconcile

The second is also faster on a good network, because the UI never
waits for a round trip it does not need. Offline capability is the
by-product of an architecture whose main benefit is responsiveness.
  • Every write goes to the local store first and is reflected immediately
  • Changes are queued with enough context to be replayed, including the client's timestamp and a client-generated identifier
  • Sync is a background process that can fail, retry and resume without affecting what the user is doing
  • Identifiers are generated on the device, so a record exists and is referenceable before the server has ever seen it

The sync queue, which is where the bugs live

A queue of pending changes sounds simple and contains most of the difficulty. Each property below is one that gets discovered the hard way if it was not designed in.

  1. Durable. The queue survives the app being killed, the device restarting and the battery dying — it lives in persistent storage, not memory.
  2. Ordered per entity. Changes to one record apply in the order they were made; unrelated records need not block each other.
  3. Idempotent on the server. A change may be sent twice when a response is lost, so each carries a client-generated key the server deduplicates on.
  4. Bounded. A queue that grows without limit on a device offline for a fortnight eventually fills the storage — decide what happens then, deliberately.
  5. Inspectable. The user can see that something is pending, and support can see what is stuck.
  6. Poison-resistant. One change the server permanently rejects must not block every change behind it forever.

The last is the failure that produces the worst support cases. A single malformed or now-invalid change at the head of a strict queue silently stops all synchronisation, and the user's subsequent work accumulates unsent while everything appears to be working.

Conflicts, which you must decide about explicitly

Two devices edit the same record while offline. Both sync. Something has to give, and the default — whichever arrives last wins — silently discards someone's work.

StrategyFitsLoses
Last write winsLow-stakes fields, single-user dataThe earlier edit, silently
Field-level mergeRecords where fields are independentNothing, if fields genuinely are independent
Append-only eventsLogs, messages, measurementsNothing — there is no conflict to have
Server authority on specific fieldsStock levels, balances, anything contendedThe client's optimism, correctly
Ask the userDocuments and long-form contentTheir time — reserve for genuinely valuable edits

Tell the user what state things are in

The interface must never imply something is saved to the server when it is queued on the device. That single dishonesty is what turns a sync delay into lost trust when the phone is later wiped or reinstalled.

  • A per-item state that distinguishes saved locally, syncing, synced and failed — not a single global spinner
  • The age of displayed data, shown plainly, so a stale price or stock figure is understood as stale
  • A visible, actionable path when something fails permanently, rather than an indefinite retry with no explanation
  • An explicit warning before any action that would discard unsynced work, including sign-out
  • Language that says what happened: "saved on this device, will sync when connected", not "saved"

Test the states that actually occur

  1. Airplane mode for a full session: create, edit, delete, then reconnect and verify everything arrived exactly once.
  2. A request interrupted mid-flight — the response lost after the server acted. The record must not be duplicated.
  3. Two devices, same account, conflicting edits offline, then both reconnect.
  4. A captive portal that returns HTTP 200 with a login page for every request, which defeats naive connectivity checks.
  5. The app killed with a full queue, then relaunched — nothing may be lost.
  6. A permanently rejected change in the queue, confirming everything behind it still syncs.

The captive portal case is worth automating. A connectivity check that only asks whether an interface is up will report connected on a hotel network that answers every request with a login page, and the app will retry against nonsense indefinitely.

Questions

Can offline support be added to an existing app?

Rarely without significant rework, because offline is an architecture rather than a feature: it determines whether the local store or the server holds the truth, and that decision shapes every screen, write and piece of state. What can be added incrementally is graceful degradation — showing cached data with its age instead of an error — which delivers much of the perceived quality at a fraction of the cost.

Why do identifiers need to be generated on the device?

Because a record that only receives an id when the server responds cannot be referenced, linked or edited until the network returns — which makes offline creation impossible. Generate a UUID or similar on the device and have the server accept it. Retrofitting this means touching every table and every relationship, so it belongs in the first schema rather than the second.

How should sync conflicts be resolved?

Decide explicitly rather than accepting the default, which is that whichever write arrives last silently discards the other. Field-level merge works where fields are independent; server authority is correct for contended values like stock and balances; asking the user suits documents. Better still, shape the data so conflicts cannot occur — recording "added 3" rather than "quantity is now 8" makes two offline edits combine instead of compete.

What is the most damaging offline bug?

Signing out with unsynced work in the queue. Sign-out typically clears local data, so the app destroys work the user believed was saved, unrecoverably. Close behind is a permanently rejected change at the head of a strict queue, which silently blocks all subsequent synchronisation while the app continues to look like it is working.

Want this applied to your systems?

Most of what is described here begins as an assessment: a fixed-scope review producing a prioritised written report you can act on with any supplier.

Prefer email? hello@kornspire.com