Mobile · Mid-level

Offline-first and sync: the app that works without a network

Mobile connections drop in elevators, on the subway, out in the countryside. An app that only works with a perfect network frustrates the user at the worst possible moment. The offline-first strategy flips the logic: the app operates on local data and treats the network as a luxury that syncs whenever it shows up. Modeling this well — with a pending queue and conflict resolution — is a classic mid-level skill.

Why offline-first

Instead of "try fetching from the network; if it fails, show an error", offline-first says "read and write locally; the network is just a background detail". The user stays productive on the plane, and when the connection returns, everything sorts itself out. The gain isn't only offline: even online, reading from local storage is instant, with no loading screens.

Read and write locally first

The local database (see state and data on mobile) becomes the immediate source of truth for the UI. When the user creates an item, it shows up right away — saved locally — without waiting for the server. The network comes in later, in the background.

The analogy: it's like jotting down orders in a notebook during the day and typing everything into the system at night. The service (UI) never stops waiting for the central system.

The pending operations queue

Every write that hasn't reached the server yet goes into a pending queue persisted locally. "Create order 42", "mark task as done", "delete note". The queue survives the app being closed and is processed in order when there's a network.

queue = [
  { op: "create", type: "order", id: "tmp-1", data: {...} },
  { op: "update", type: "task", id: 7, field: "done" }
]
// when the network returns: send one by one, remove on confirmation

Offline → sync flow diagram

  [ User acts ]
        |
        v
  [ Write to local database ]  --> UI updates RIGHT AWAY
        |
        v
  [ Enqueue pending op ]
        |
   (no network) ... wait ...
        |
   (network returns)
        v
  [ Send queue to server ] --> success? remove from queue
        |                       conflict? resolve
        v
  [ Server confirms / returns state ]

Syncing when the network returns

You observe connectivity and trigger sync on reconnect (and, ideally, periodically). Sync is bidirectional: it sends your pending operations and downloads what changed on the server since last time (delta sync, using a timestamp or cursor).

Conflict resolution

The hard problem: you edited a record offline and someone else edited the same record on the server. Who wins?

StrategyHow it worksRisk
Last-write-winsThe most recent write (by timestamp) overwritesSilently loses the other edit
Field-level mergeCombines changes to different fieldsComplex; conflicting fields still need a decision
Resolve with the userShows the versions and asksUX friction; only for critical cases

Interviewers often ask you to explain last-write-wins and when it does not fit — for example, a financial balance, where blindly overwriting loses money.

Request idempotency

Since the queue may resend an operation (the network dropped right as the response was coming), the server must tolerate receiving the same thing twice without duplicating it. The solution is idempotency: each operation carries a unique identifier (idempotency key) and the server ignores repeats.

POST /orders
Idempotency-Key: tmp-1-abc
// resending with the same key does not create 2 orders
Treat the network as something that can fail at any moment, including after processing but before responding. Idempotency and a persisted queue are what make sync reliable, not optional.

Conclusion

Offline-first means local first, network later: write to the database, enqueue the pending operation, sync on reconnect and resolve conflicts with a conscious strategy. Add idempotency to survive resends, and you deliver an app that works in the elevator and sorts itself out when the signal returns.

Read also

Back to the track