Engineering

Offline first is a product decision, not a caching layer

Every team asks for offline support after the first field report. The hard part was never the cache — it is deciding what the app is allowed to promise.

Written by Denis Rudenko, Software Engineer · · 2 min read

Every field team asks for offline support after their first week without signal. The request sounds like a caching problem. It never is.

Four questions before any code

  1. When two devices edit the same record offline, which one wins?
  2. What is the app allowed to promise a user who is offline right now?
  3. How long may stale data stay on screen before it becomes a lie?
  4. What happens to a queued action the server later rejects?

Teams that answer these four questions first ship offline support in weeks. Teams that start with a cache library spend months discovering the questions one bug at a time.

Conflict resolution is a product rule

Last write wins is a decision, not a default. On a logistics app a driver's delivery confirmation must never be overwritten by a dispatcher's stale edit, so the record carries an origin and the merge rule reads it.

fun merge(local: Delivery, remote: Delivery): Delivery = when {
    local.confirmedByDriver && !remote.confirmedByDriver -> local
    remote.updatedAt > local.updatedAt -> remote
    else -> local
}

Show the state, do not hide it

The apps that feel trustworthy offline are the ones that say what they know:

The moment an app pretends everything is fine offline, the user stops believing it when it says everything is fine online.


What it costs

ApproachBuild timeSupport load
Cache on top of a network app2 weeksHigh, forever
Local first with sync6 weeksLow

The second column is the one that decides it. A cache bolted onto a network first app generates support tickets for the life of the product.