Android
Data Synchronization
Webserver Integration
Mobile App Development
API Communication

Sync data between Android App and webserver

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Synchronizing data between an Android app and a web server is not one feature but a system design problem. A good sync design decides where truth lives, how changes are queued offline, how conflicts are resolved, and how background work is retried safely.

Start with a Clear Sync Model

Before writing any networking code, decide what the app is actually synchronizing.

A practical default for many business apps is:

  • keep a local database on the device
  • treat the server as the long-term source of truth
  • record local pending changes until they are acknowledged
  • periodically pull remote changes and merge them locally

This gives the app offline capability and avoids making every screen depend on a live network response.

Use Room Locally and Retrofit for the API

On Android, a common and maintainable stack is:

  • Room for local persistence
  • Retrofit for HTTP API calls
  • WorkManager for background sync

A minimal Room entity might look like this:

kotlin
1import androidx.room.Entity
2import androidx.room.PrimaryKey
3
4@Entity(tableName = "notes")
5data class NoteEntity(
6    @PrimaryKey val id: String,
7    val title: String,
8    val updatedAt: Long,
9    val isPendingSync: Boolean
10)

The isPendingSync flag is one simple way to mark rows that still need to be sent upstream.

Design the Server API for Sync, Not Just CRUD

A pure CRUD API can work, but sync gets easier if the server supports incremental changes.

Useful patterns include:

  • 'GET /notes?updatedAfter=...'
  • 'POST /notes/sync with a batch of local changes'
  • version numbers or updatedAt timestamps on every record

Retrofit interface example:

kotlin
1import retrofit2.http.Body
2import retrofit2.http.GET
3import retrofit2.http.POST
4import retrofit2.http.Query
5
6interface NotesApi {
7    @GET("notes")
8    suspend fun getChanges(@Query("updatedAfter") updatedAfter: Long): List<NoteDto>
9
10    @POST("notes/sync")
11    suspend fun pushChanges(@Body changes: List<NoteDto>)
12}
13
14data class NoteDto(
15    val id: String,
16    val title: String,
17    val updatedAt: Long
18)

This is more sync-friendly than forcing the client to re-download the entire dataset every time.

Run Sync in WorkManager

Do not tie all synchronization to a visible screen. Background sync belongs in WorkManager, which handles retries and device constraints better than ad hoc coroutines launched from activities.

kotlin
1import android.content.Context
2import androidx.work.CoroutineWorker
3import androidx.work.WorkerParameters
4
5class NotesSyncWorker(
6    appContext: Context,
7    params: WorkerParameters,
8    private val repository: NotesRepository
9) : CoroutineWorker(appContext, params) {
10
11    override suspend fun doWork(): Result {
12        return try {
13            repository.pushPendingChanges()
14            repository.pullRemoteChanges()
15            Result.success()
16        } catch (ex: Exception) {
17            Result.retry()
18        }
19    }
20}

This worker structure is simple, but it captures the right flow: upload local pending work first, then fetch remote updates.

Make the Repository Merge Deterministic

The repository should be the place where API responses and local state are reconciled.

kotlin
1class NotesRepository(
2    private val api: NotesApi,
3    private val dao: NotesDao,
4    private val clock: () -> Long
5) {
6    suspend fun pushPendingChanges() {
7        val pending = dao.getPendingNotes()
8        api.pushChanges(pending.map { NoteDto(it.id, it.title, it.updatedAt) })
9        dao.markPendingAsSynced(pending.map { it.id })
10    }
11
12    suspend fun pullRemoteChanges() {
13        val lastSync = dao.getLastSyncTimestamp() ?: 0L
14        val remote = api.getChanges(lastSync)
15        dao.upsert(remote.map { NoteEntity(it.id, it.title, it.updatedAt, false) })
16        dao.setLastSyncTimestamp(clock())
17    }
18}

The details depend on the app, but the important point is to centralize merge behavior instead of spreading it across activities, fragments, and view models.

Plan Conflict Resolution Early

If both the device and server can edit the same record, conflicts will happen. The worst time to think about conflict resolution is after users already depend on the app.

Common strategies:

  • last write wins
  • server wins
  • client wins
  • field-level merge
  • manual conflict UI

For many business apps, last-write-wins with a clear updatedAt or version field is enough. For collaborative or high-value data, you usually need something more explicit.

Avoid Real-Time by Default

Not every app needs WebSockets or live streaming. Polling plus background sync is often enough and dramatically easier to operate.

Use real-time channels only when the product genuinely needs near-instant updates, such as chat or collaborative editing. Many ordinary mobile apps can sync well with:

  • local writes immediately saved in Room
  • background upload via WorkManager
  • periodic or on-resume refresh from the server

That is a far more robust default than building a fragile always-connected sync layer.

Common Pitfalls

The biggest mistake is making the UI depend directly on the network and calling that “sync.” That produces an online-only app, not a synchronized app.

Another mistake is skipping a local database. Without durable local state, retries, offline edits, and merge logic become much harder.

Developers also forget conflict strategy until duplicate edits start overwriting one another in production.

Finally, avoid firing network sync from random lifecycle callbacks with no retry strategy. Background sync should be explicit, queued, and observable.

Summary

  • Good sync starts with a clear ownership and conflict model, not with a networking library choice.
  • Room, Retrofit, and WorkManager are a strong default stack for Android-to-server synchronization.
  • Keep local state on the device and upload pending changes in the background.
  • Design the server API for incremental changes instead of full re-downloads.
  • Decide conflict resolution rules early so data consistency does not become accidental.

Course illustration
Course illustration

All Rights Reserved.