PerspectivesReference architecture

Offline-first mobile architecture: patterns and failure modes

Offline-first mobile architecture makes the device’s local database the source of truth and treats the network as a channel for synchronising, not a dependency. Writes go to a local queue, carry client-generated IDs and idempotency keys, and sync when connectivity returns. Retrofitting it onto an online app usually fails at identity, conflicts and migrations.

Author
NextSense EngineeringEngineering team
Published
Reading
6 min

Offline is a design decision, not a feature

Most mobile apps treat the network as always present and add a cache when users complain. That works while connectivity is good and failures are rare. It stops working in a warehouse basement, on a farm at the edge of coverage, on a delivery route through patchy signal, or anywhere the cost of a lost action is higher than a retry. In those places the network is not an occasional absence; it is the normal condition, and an app that assumes otherwise fails in ways its users cannot see until their work is gone.

Offline-first architecture inverts the assumption. The device holds the data the user needs, the interface reads from the device, and the network becomes a channel for synchronising state rather than a dependency every screen waits on. The shift sounds small. It changes where data lives, how records are identified, what a write means and how the product behaves when two people change the same thing — which is why it is far cheaper to decide at the start than to retrofit.

The shape of the architecture

The reference architecture below lists the layers an offline-first data layer needs, what each is responsible for, and the failure that appears when the layer is missing — usually because the app began online-only and had offline behaviour added later.

Reference architecture

Layer

Responsibility

Failure mode when retrofitted

Local store

The source of truth the interface reads from, typically SQLite through a platform or cross-platform wrapper

The app still reads from the network and caches, so every screen has two code paths

Outbox

Every write becomes a durable, ordered operation before anything is sent

Writes go straight to the API and are lost when a request fails mid-flight

Identity

Records receive client-generated IDs, such as UUIDv7, when created

Server-assigned IDs force placeholder IDs and a rewrite of every reference

Idempotency

Each operation carries a key the server uses to ignore repeats

Retries create duplicate orders, payments or submissions

Sync engine

Pushes the outbox and pulls changes since a cursor, in the background

Full refreshes on every launch, and battery use the platform will throttle

Conflict policy

Decides, per data type, what happens when two edits meet

The last write silently wins and users lose work without knowing

Schema migration

Moves local data forward without losing queued writes

An app update wipes the local database, and the unsynced work with it

Session

Lets the user keep working when a token expires offline

Users are signed out in the field and their queue is stranded

Observability

Reports queue depth, sync lag and failures from devices

Problems surface as support tickets weeks after they began

Patterns that hold up

Write locally, then queue

A user action writes to the local store and appends an operation to an outbox in the same transaction. The interface updates immediately from the local store. A sync worker drains the outbox in order when connectivity allows. This outbox pattern means an action is never half-done: either both the local change and the queued operation exist, or neither does.

Generate identity on the device

When the server assigns IDs, an offline record has no permanent identity, and every reference to it — a line item on an order, a photo attached to an inspection — must be rewritten once the server replies. Generating time-ordered IDs on the device removes the problem at the root. Records are addressable from the moment they exist, and references never change.

Make every operation idempotent

Mobile networks fail after a request is sent and before the reply arrives. The client cannot tell whether the server acted, so it retries. Each queued operation therefore carries an idempotency key, and the server records the keys it has processed and returns the original result for a repeat. Without this, retries become duplicates, and in a payments or ordering flow duplicates are the most expensive bug there is.

Choose conflict policy per data type

There is no single correct conflict strategy. Preferences can take the last write. Independent fields on a record can merge field by field. Stock counts, balances and anything involving money should be server-authoritative, expressed as operations rather than overwritten values. Collaborative text may justify conflict-free replicated data types. The mistake is choosing one policy for everything, usually last-write-wins by default, and discovering its cost in the one table where it was wrong.

Sync deltas, not snapshots

Pull only what changed since the device's last cursor, and push only queued operations. Full refreshes are simple to build and ruinous at scale: they multiply server load by the number of devices and drain batteries that the operating system will then throttle.

Failure modes to design out

Trusting the device clock

Device clocks drift, change time zones and are sometimes set by hand. Ordering edits by device time produces conflicts resolved the wrong way round. Order by server-issued versions or hybrid logical clocks, and keep device time for display only.

Queues that outlive app versions

A queued operation written by version 3.1 may be sent by version 3.4. Operations need a version, the server must accept older shapes for a transition period, and migrations must carry the outbox forward rather than discarding it.

Media and large payloads

Photos and documents do not belong in the same queue as small records. They need their own resumable upload path, with the record referencing the file by its client-generated ID, so a slow upload never blocks the data behind it.

Background limits

Both platforms schedule background work on their own terms. Sync that relies on running whenever the app wishes will be deferred or killed; work with the platform schedulers and make every sync step safe to interrupt.

Renting the sync layer

Managed sync services are convenient and occasionally withdrawn — MongoDB deprecated Atlas Device Sync in 2024. If a vendor provides sync, keep the local data model and the operation format your own, so the exit is a new transport rather than a rewrite.

Testing it properly

Offline behaviour fails in sequences, not single requests, so test sequences: switch to aeroplane mode mid-write, kill the app during sync, edit the same record on two devices, update the app with a non-empty queue, and let a session expire while offline. Run these on real devices under network conditioning, and keep them in the regression suite.

Then measure what users experience in the field: how long a queued action waits before it syncs, how often a sync fails and is retried, and how large queues grow on the devices that stay offline longest. Those numbers show whether the architecture is working where it matters, not just in the test lab.

This is the data layer we build for field and mobile products across our Digital Products work, and it is what lets on-device models in Applied AI keep working where there is no signal at all — a constraint we meet most often in agriculture technology.

Questions, answered

What is the difference between offline-first and caching?

A cache stores copies of server data so screens load faster; the server stays the source of truth and writes still need the network. Offline-first makes the device the source of truth for the user’s work, so reads and writes both succeed without a connection and sync later.

How are conflicts resolved when two people edit the same record?

By a policy chosen per data type: the last write for preferences, a field-by-field merge for independent fields, server-authoritative operations for money and stock. One default policy for everything is the most common mistake.

Can an existing online app be made offline-first?

Yes, but it is closer to rebuilding the data layer than adding a feature. Record identity, the write path and conflict handling all change, so it is worth planning as a project rather than a sprint.