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?
| Strategy | How it works | Risk |
|---|---|---|
| Last-write-wins | The most recent write (by timestamp) overwrites | Silently loses the other edit |
| Field-level merge | Combines changes to different fields | Complex; conflicting fields still need a decision |
| Resolve with the user | Shows the versions and asks | UX 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.