Back to all posts
·5 min read

Designing reliable sync for offline-first mobile apps

On mobile, connectivity is rarely a binary state. A person can keep using an app on a train, under weak Wi-Fi or while the device moves between networks. Even when the connection indicator is visible, a request can be delayed, time out, or lose its response before it reaches the device. Offline behaviour is therefore not only a matter of designing an error screen; it is a foundational part of the data model and the user experience.

In an offline-first design, the app reads the data it displays and changes from a local store first. The network is not a prerequisite for the interface to work. It is a channel that reconciles local changes with the server and other devices. The goal is not merely to show something when there is no internet. The goal is for users to see the result of their actions immediately while data is safely synchronised later.

Local data is more than a temporary cache

In an online-first app, local storage is often treated as a cache: fetch fresh data from the server when the network works and show old data when it does not. In an offline-first design, the local database is the application’s working source. Screens query it, and new records, edits and deletions happen there first.

This creates two benefits. Interaction latency stops depending on the network, so a new lesson, corrected note or completed activity appears immediately.

Put identity, versioning and deletion into the data model

Every record that will be synchronised needs a durable identifier independent of the device. A numeric identifier assigned later by the server is not enough to create a new record while offline. Client-generated identifiers, such as UUIDs, can remain stable throughout a record’s lifecycle.

Records also need fields that explain the order of changes. updatedAt alone is often insufficient: device clocks can differ, and concurrent edits can make ordering ambiguous. A server-issued version number, logical clock or revision marker makes it clear which change a client has seen.

Deletion needs the same care. Removing a record immediately from one device prevents other devices from learning that it was deleted. A soft-delete field such as deletedAt or isDeleted lets the deletion event synchronise. The record can be physically removed only after a retention period that is safe for active clients.

Separate writes with a change log

When every user action becomes a direct network request, it becomes difficult to know whether an operation was sent again after a connection failure. A more reliable pattern writes an action first to a change log alongside the local record. The log can contain the operation type, record identifier, prior version, retry count and an idempotency key.

Background synchronisation processes this log in order. A timed-out request does not prove that the server never received it. Retrying with the same idempotency key lets the server handle the request safely without creating a duplicate record. When a successful response arrives, the log entry is marked complete and the server’s version is applied to the local record.

This pattern also gives the user interface useful clarity. Rather than only saying “saved”, it can say “saved on this device; waiting to sync” when necessary. Users know their data has not vanished, and the team can inspect which changes are queued when something goes wrong.

Conflict resolution depends on domain meaning

When two devices edit the same record, no universal rule is always correct. “Last writer wins” looks simple, but it can lose data unnecessarily when people changed different fields. One person may update contact information on a phone while another changes only the note field on a tablet.

Field-level merging is a good starting point for many products. Each field has its own modification time or revision, so only overlapping fields require a decision. Collections and lists may need different strategies: additions can merge, ordering can use a separate key, and a text change to the same item may need an explicit choice from the user.

Some data is not suitable for automatic merging. Payment amounts, quotas, lesson-package balances or appointments with time collisions should leave the server business rule as the final authority. The client can update optimistically, but when the server rejects it, the product should offer a clear correction path.

Pull and push are parts of the same protocol

Synchronisation is not only sending pending local changes to the server. A client also has to pull changes made on other devices. Saving the last seen server revision or a sync token is practical. The client asks, “what changed after this point?” and the server returns created, updated and deleted records.

The order of pushing and pulling can vary with product rules, but both directions should be safely repeatable. The same sync step must be able to start when the app closes, opens again, connectivity returns or the user asks to refresh. This property makes timing errors and unpredictable networks less critical.

Build in observability and privacy

Most sync failures are invisible on a user’s screen. Track metrics such as queue length, time of the last successful sync, retry count, conflict rate and server rejections. Use technical identifiers that allow an event chain to be followed without writing record contents or sensitive personal data to logs.

When a local store contains personal information, device security, encryption, cleanup after sign-out and the ability to cut access after an authorisation change also belong in the design. Being offline-first does not mean ignoring authorisation rules offline.

Good synchronisation hides network complexity from users, but not from the system. When local records, a change log, versioning and domain-appropriate conflict rules are designed together, a mobile app remains reliable and calm even as connectivity changes.