Offline-first and sync

How do you design an offline-first architecture?

You design an offline-first architecture by making a local store the only place the interface reads from and the first place it writes to, then adding a sync layer that exchanges changes with the network when a path exists. The main decisions are what data lives on the device, which actions may complete offline, how queued changes are retried and merged, and which system has the final say.

Learning objectives

After reading this article you will be able to:

  • Explain why the local store should be the only source the interface reads
  • Compare online-only, queued and lazy writes for different kinds of action
  • Describe the tools Android, Apple platforms and the web offer for retries

Start from the local store

The core decision in an offline-first architecture is where the interface gets its data. Android’s guide to building an offline-first app puts it plainly: the local data source is the canonical source of truth for the app, and it should be the only source that higher layers read from. The network is a second data source that a repository uses to update the local one.

This gives a simple rule for every screen. It observes local data and redraws when that data changes, whether the change came from the person’s own action or from a sync that just finished. No screen calls the network directly, so no screen has an error state that only exists because a request failed.

The local store is usually an embedded database such as SQLite, or IndexedDB in a browser. Android’s guide also recommends keeping separate models for what the network sends and what the database stores, so a change to the server’s format does not spread through the app.

Decide what lives on the device

A device cannot hold everything, so decide what each kind of person needs while disconnected. A technician needs today’s jobs and their manuals; a reader needs the articles they opened. The scope determines how sync fetches data.

Android describes two approaches. In pull-based sync the app fetches data when a screen is about to show it, which is simple but leaves screens empty or stale after a long time offline. In push-based sync the app downloads a baseline on first start and then fetches only what the server says has changed, which lets it stay offline far longer but needs a server that supports synchronization. Many apps mix the two by kind of data. How does offline sync work? covers what happens when the copies meet.

Choose a write strategy per action

Writes need more thought than reads, because a write made offline can be rejected later. Android names three strategies:

  • Online-only writes. Try the network first and update the local store only on success. Its example is a bank transfer. When offline, disable the action or say clearly that it needs a connection.
  • Queued writes. Put the write in a queue and send it later, accepting that it may never arrive. Suitable for analytics events and logging.
  • Lazy writes. Write to the local store first, then queue the change for the network. This is the right choice for data the person cannot afford to lose, such as tasks in a to-do app, and it is where conflicts come from.

Lazy writes mean the screen shows a change before any server has seen it, which is an optimistic update. The queue of pending changes is an outbox. Because the app will retry sends whose outcome it never heard, each change needs a stable identifier so the receiver can apply it once; this is idempotency.

Plan sync, retries, and conflicts

The sync layer drains queues when a connection exists and retries with growing delays when it fails. Each platform has tools for this:

  • Android. WorkManager runs persistent work with a network constraint and retries failed syncs with exponential backoff.
  • Apple platforms. A URLSession configured to wait for connectivity holds a request until a connection is available instead of failing at once, and background sessions always wait.
  • Web. A service worker can answer requests from a cache, following patterns such as cache falling back to network or stale-while-revalidate. The Background Synchronization API lets a page defer requests until the network returns, but MDN marks it as not Baseline because it does not work in some widely used browsers.

When two copies change the same data apart, the app needs a rule decided in advance. Android notes that last write wins is common on mobile; other data is better merged with a CRDT or sent to a person. How are sync conflicts resolved? compares the options.

Keep an authority and show the truth

Offline-first does not remove the server’s role. Sync makes copies agree, but it cannot check that stock is not claimed twice or that a person was allowed to approve something. Those rules need an authority, usually the backend. Offline Protocol’s documentation describes the same split for device-to-device work: the system of record remains authoritative for the records it owns, and the app should define when the backend has durably accepted a record before clearing its local queue.

The interface should match. Show what is saved on this device, what is waiting to send, and what the server has confirmed, and give people a way forward when a queued change is rejected. Delivered, accepted, committed explains why these are different moments.

Design the tests with the architecture

Each decision above creates a case to test: a cold start with no network, a write queued for days, a send that drops halfway, a conflict, a rejection. Write these down while designing, not after. How do you test an app for offline behaviour? lists the tools and scenarios.

Frequently asked questions

Does an offline-first app have to support writes offline?

No. Android's guidance says an offline-first app must at least be able to read without network access. Many apps allow offline writes for most actions but keep some, such as payments, online-only.

Where should the network code live?

Behind the data layer. The screens read from the local store and send actions to it; a repository or sync component talks to the network and updates the local store, so the interface never depends on a request succeeding.

Sources

Build it with Offline Protocol

The page on what the SDK handles divides the work between the device-to-device layer, your application, and your backend, and explains when a local queue can be cleared once the backend has accepted a record.

Read what the SDK handles