All posts Backend

Building an App That Works Offline, and Resolving Conflicts

Offline support does not just mean "keep the data locally". The hard part is deciding what happens when both sides change the same record.

DT
Demir Taşdemir Mobile App & Web Developer
— min read

An app that works offline looks like magic to the user: everything is still there with no internet, and it quietly falls into place once the connection is back. On the developer side, though, it is one of the things that deserves the most thought.

Because storing the data locally is the easy part. The hard part is this question: if both sides changed the same record, which one wins?

First, get this straight: not all data is the same

Every piece of data in an app does not have to behave the same way offline. I split data into three groups:

TypeExampleOffline behavior
The user's own Personal notes, settings Full offline writes. Conflicts almost never happen.
Shared Team tasks, a shared list Offline writes are possible, but a conflict strategy is a must.
Owned by the server Stock, balance, payment Never allow offline writes. Queue them and let the server decide.

Making this distinction up front removes a whole class of problems that is very hard to fix later. If you allow the balance to be decreased offline, the user can spend more than they have while in airplane mode.

The default strategy: last write wins

Most tools do this by default: the write that arrives last overwrites the previous one. It is easy to implement, but it loses data silently. The user only notices days later that the note they wrote is gone, and never understands why.

Do not trust the device clock

If you decide "who wrote last" based on the device clock, a single device with a wrongly set clock can break the whole synchronization. The server should generate the timestamp; keep the time sent by the client for information only, never as the record of truth.

Better: field-level merging

Most conflicts are not really conflicts. The user changed the task's title while offline, and you changed its due date from the panel. Merging only the changed fields, instead of overwriting the whole document, keeps both.

// Send only the changed field instead of the whole document
// (update in Firestore, the same idea as PATCH in REST)
db.collection("gorevler").document(id)
  .update(mapOf("baslik" to yeniBaslik))

To do this, the client has to know what changed. Keeping a local record of "these fields of this item changed" is more work than sending the whole object, but the result is far more solid.

Keep pending operations in a queue

Storing offline changes as operations rather than as plain "data" makes things easier. When the connection returns, the queue is processed in order.

// Store the intent, not the data
{ "islem": "gorev_ekle",     "id": "g_918", "veri": { ... }, "zaman": 1723... }
{ "islem": "gorev_guncelle", "id": "g_204", "alanlar": ["baslik"] }
{ "islem": "gorev_sil",      "id": "g_177" }

This structure makes sure that ordered operations such as "add first, then delete" are applied correctly. Retrying a single failed operation is also much cheaper than running the whole synchronization from scratch.

Operations must be repeatable

When the network drops, it is very common for the same request to go out twice. If the server takes it for a second operation, the same record gets created twice.

The fix is to give every operation a unique id generated on the client. When the server sees the same id a second time, it does not apply the operation again; it simply returns the previous result.

Show the user the status

Offline support does not build trust unless you keep the user informed. A simple status indicator goes a long way:

  • Saved — it reached the server.
  • Pending — it is on the device and will be sent once there is a connection.
  • Failed to send — something went wrong, and the user can try again.

Without that indicator, the user cannot tell whether the data was saved, so they repeat the same action over and over — and that is when the real mess begins.

On a real conflict, ask the user

If a conflict cannot be resolved automatically — both sides changed the same field in different ways — the most honest path is to show the user both versions and let them choose, instead of silently picking one. It rarely happens, but when it does, the user has not lost any data.

Offline support is an investment that goes completely unnoticed in the happy path but saves the entire product in the bad one. The hard part is not writing it, it is deciding up front what should happen.

  • Offline
  • Synchronization
  • Conflicts
  • Architecture
  • Data model
Share: LinkedIn X WhatsApp
DT

Demir Taşdemir

Mobile App & Web Developer

I have been building software since 2018. I have published 11 apps on the App Store and Google Play; right now I am working on 6 mobile apps, 1 e-commerce platform and 1 desktop game.

An approach that does not skip the hard part

Anyone can write the happy path. What keeps a product standing is having thought through the edge cases.