An app without a signal: designing offline data and synchronisation

Local data, pending operations, retries and conflicts. A practical design for a mobile app that preserves a user's work when the connection fails.

A person using a mobile phone at a desk

Decide which tasks need to work offline

Consider a town calendar and a form for reporting a damaged bench. Someone checks the waste collection date in a garage with no signal, then takes a photo of the bench and writes a report. The calendar can show its most recently downloaded version. The report must stay on the phone when the app closes. Confirmation that the town has received it, however, requires a server response. Agree on these three rules before designing the screens.

Android's architecture guidance separates local and network data sources and recommends reading UI data through the local source. This does not decide which writes your product should allow offline. For each task, document its freshness requirement, what happens when the local store is empty and the consequences of delay. These decisions affect the cost of synchronisation more than the choice of library.

TaskWithout a connectionWhen the connection returns
Read the calendarLast stored version and update timeFetch changes
Create a reportStore a draft or pending reportSend it and store the receipt
Confirm a bookingExplain that a connection is requiredCheck server-side availability

Show the real state of the data

The local database may contain published content, unfinished user input and a queue of unsent operations. Each has its own lifecycle. A notice saved yesterday is not in the same state as a report awaiting acceptance. Useful report states include draft, pending, sending, accepted and needs correction. A single connection icon cannot explain these differences.

Separate displaying stored content from checking for a newer version. If the refresh fails, keep older data usable and show the last successful update time clearly. An initial launch with no stored content needs a different empty state. Users can then distinguish an unloaded calendar from a calendar with no events, and a form saved on their phone from a successfully submitted report.

Store the change and its pending operation in one transaction

When the user saves a report, write both the report and its pending queue entry in one database transaction. If the process stops between two independent writes, the report can exist without an operation that will send it. A transaction removes this intermediate state. SQLite provides atomic transactions under its documented assumptions; your application must still handle a full disk and write failures, and cannot confirm a save that failed.

Give the operation a stable ID that stays unchanged across retries. The server must check that ID within the authenticated account or organisation and retain the outcome of previously processed writes. The example describes a proposed queue model, rather than a universal wire protocol. For a photograph, store a durable reference to the local file and track whether the attachment has already been uploaded.

Illustrative local queue entry; these fields belong to the example design.json
{
  "operationId": "op-7d2c",
  "entityId": "report-84",
  "kind": "create-report",
  "baseVersion": null,
  "payload": {
    "category": "street-furniture",
    "description": "Damaged bench"
  },
  "attachment": "local://photo-84",
  "state": "pending",
  "attempts": 0
}

A retry must not create a second report

The most awkward failure occurs after the server commits the write but before the phone receives the response. The app cannot tell whether the report was accepted. Its next attempt sends the same operation. The server returns the original result for that operation ID, so neither the citizen nor the town receives a duplicate report. An identifier without server-side deduplication does not provide this property.

Retry temporary network errors or overload with increasing delays and jitter. Move invalid input to a state that requires correction; an expired login requires renewing the session. Decide what happens to unfinished work at logout: it might stay bound to the original account or be removed. A different account must never inherit someone else's queued writes. Preserve operation order wherever a later step depends on the result of an earlier one.

Conflict resolution is a decision about the data

If two devices edit the same record, the last write may overwrite the other person's work. Last write wins is simple, but the phone's clock may also be wrong. Overwriting a minor preference can be acceptable. For a report description, an approval or an assignment, you need to know which version the edit was based on.

The server can issue an ETag and require an If-Match header on an update. RFC 9110 defines this condition for checking the current representation. A mismatch lets the server reject a stale write. The app can then load the current record and offer a comparison or let the person resubmit a revised edit. Automatic merging only makes sense for fields with clear rules: two independent notes can be appended, while two conflicting approvals require knowledge of the workflow.

Synchronise deletions and returns after a long absence

Downloading the entire list whenever a screen opens may be simpler than building a change protocol. For larger datasets, the server and client can exchange an opaque cursor. Commit a page of changes and its new cursor together; a crash will then not cause the client to skip records it never saved. The client must also process deletion markers, or an old notice will keep reappearing from local storage.

A cursor cannot have an unlimited lifetime just because the app might remain closed for months. Define a full refresh when it expires. Protect unsent user input before that refresh so clearing downloaded content cannot erase it. Version the local database schema and test migrations with pending operations present. A new app version needs to understand the queue created by its predecessor.

The operating system decides when background work can run

Android's WorkManager can preserve scheduled tasks across app and device restarts, but execution still depends on system conditions and restrictions. On iOS, Apple does not guarantee delivery of silent background notifications and may throttle them. The design consequence is to check for changes when the app returns to the foreground and provide a manual refresh.

Compare a request to synchronise every minute against the available mechanisms, battery cost and data volume. Reading notices, uploading a photo and confirming a time-sensitive booking do not need the same strategy. The queue is therefore a durable record of work; the system scheduler provides an opportunity to perform it. Keep a pending report visible even after a long delay and attempt to send it when the app opens, using the current authentication state.

Test the points where work could be lost

Inject failures between individual steps, rather than only before pressing save. Disconnect after a request leaves the device, terminate the process after local confirmation and modify the server record during an offline edit. For each case, define the expected record count, queue state and user-facing message. The following cases are proposed tests, not measured results from a client implementation.

  • The server accepts a report but its response is lost: retrying the same ID produces one report in total and the app obtains its receipt.
  • The phone restarts with a pending photograph: both text and file remain available, and sending resumes without a duplicate attachment.
  • Another account signs in on the same device: it cannot see the previous user's report or queue.
  • Two devices edit the same version: the server and UI apply the agreed conflict rule without silently discarding an edit.
  • The cursor expires during a database update: a full refresh preserves unsent work, removes old content and records the new cursor.

Sources and documentation

For implementation, consult the documentation for the version you use.

Put the topic into practice.

Related project: Municipality of Smolenice

Have a process
that needs to change?

Let’s start with how you work today. We’ll choose the technology around it.

Discuss your project