Offline-first and sync

What happens to a write made offline?

A write made offline is saved to a local store on the device first and marked as pending. When a connection or a nearby device becomes available, the app sends it, and the other side accepts it, merges it with other changes, or rejects it. Until that answer arrives, the write is final on the device but provisional everywhere else.

Learning objectives

After reading this article you will be able to:

  • Describe the path of an offline write from local transaction to final outcome
  • Explain why queued changes need a durable outbox and stable identifiers
  • Identify which writes should wait for a connection instead of being queued

It lands on the device first

In an offline-capable app, a write does not start as a network request. It starts as a change to a local store, usually a database inside the app. Android’s offline-first guide calls this a lazy write: write to the local data source first, then queue the write to notify the network at the earliest opportunity. It recommends this for data that is critical to the app, such as tasks someone adds to a to-do list while offline.

The local write should be a transaction. SQLite, an embedded database that apps use for local storage, makes a transaction atomic: either all of its changes happen or none do, even if the operating system crashes or the power fails partway through. That matters because a write made offline may sit on the device for hours before anything else sees it. If the save itself is not durable, nothing later can recover it.

It waits in a queue

Saving the data is half the job. The app also has to remember that the change still needs to go somewhere. It records the change in a durable queue, known as an outbox, stored on disk rather than in memory so that it survives the app closing or the phone restarting.

Each queued change should carry an identifier that stays the same across retries. If the app sends the change, loses the connection before hearing back, and sends it again, the receiver can see that it is the same change and apply it once. This is idempotency, and offline apps rely on it because retries are part of normal operation.

Libraries handle part of this for you. Cloud Firestore, for example, keeps a local copy of the data the app is using and queues write operations while network access is unavailable, then synchronizes them with its backend when the device comes back online.

The app shows it, provisionally

Once the write is saved locally, the app can show it straight away. Firestore calls this latency compensation: local writes notify the app’s listeners immediately, before the data is sent to the backend. Each document carries a hasPendingWrites flag that is true until the backend confirms the write, at which point the app receives a metadata change with the flag set to false.

That flag is the important part. The person sees their change, and the app still knows the change is not settled. A well-built app keeps that distinction all the way to the screen, so that “saved on this phone” and “confirmed by the server” do not look the same. How should an app show sync status? covers how to present it.

The same caution applies to other layers. In Offline Protocol’s replicated documents, flush waits for the edit to be persisted locally; it does not mean a peer has received it.

When a path returns

When the device reaches the server again, or meets a nearby device it syncs with, the queue drains. Failed sends are retried; Android’s guide suggests draining the queue with exponential backoff, so that each retry waits longer than the last. Each write then meets one of a few outcomes:

  • Accepted as it is. Nothing else touched the same data, and the change becomes part of the shared state.
  • Merged. Someone else changed related data in the meantime, and a merge rule combines the two. How are sync conflicts resolved? covers the options.
  • Overwritten. The rule picks another change instead. For multiple changes to the same document, Firestore keeps the last write.
  • Rejected. The change breaks a rule that only the server can check, such as a stock limit or a booking that someone else already took.

A rejected write is not a lost write. The app still has the record of what the person did, and it owes them a clear message and a way forward, such as editing and resubmitting, or choosing another slot.

Some writes should wait instead

Not every action belongs in an offline queue. Android’s guide describes online-only writes for transactions that must happen in near real time, giving a bank transfer as the example. For those, it suggests disabling the action while offline or telling the person, for instance with a Snackbar.

Eric Brewer, writing in 2012 about systems that keep working when the network splits, makes a related point about actions with effects outside the system, such as charging a credit card. The safer pattern is to record the intent while disconnected and carry it out after the connection returns, as part of a workflow with an explicit pending state. The person knows they placed an order and that the system will complete it later.

What the app has to track

Taken together, a write made offline passes through states that the app should record rather than infer:

  1. Saved on the device, in a committed local transaction.
  2. Queued, with a stable identifier, waiting for a path.
  3. Sent, and possibly delivered, but not yet answered.
  4. Accepted, merged, or rejected by whoever has authority over that data.

Keeping these apart is what lets an app say honestly what has happened, retry safely, and recover when the answer is no. Delivered, accepted, committed explains why a delivery receipt alone does not settle the last step.

Frequently asked questions

Can a write made offline be lost?

It can if the app only holds it in memory, or sends it straight to the network without saving it first. Writing it to a local database in a transaction, and queueing it in storage that survives a restart, closes those gaps. It can still be rejected later, which is different from being lost.

Should every write work offline?

No. Android's guidance gives a bank transfer as an example of a write that must happen online in near real time. For writes like that, disable the action or tell the person they are offline rather than queueing it.

Sources

Build it with Offline Protocol

The local handoff guide stores each record in a durable outbox with an operation ID that survives retries and restarts, and tracks pending, delivered, accepted, committed upstream and rejected as separate states.

Read the local handoff guide