In mobile development, working with data, caching and synchronization are three key aspects that determine application performance and reliability. According to the Google Android Architecture Guide, a proper data handling architecture directly affects response speed and user experience. The Repository pattern provides a single access point to all data sources.
Key Takeaways
The Repository pattern is an architectural approach where a single repository class manages all data operations, abstracting remote REST APIs and local storage (Room or SwiftData). This way of working with data allows the application to retrieve information first from Memory Cache or Disk Cache, and then from the network, reducing response time. In mobile development, the Repository has become the de facto standard thanks to Google and Apple recommendations.
A Remote Data Source provides up-to-date information from the server via HTTP requests. Local Data Source is local storage on the device, implemented via Room on Android or SwiftData on iOS. The repository combines both sources: it first checks the local cache, and when data is absent, requests from the remote API. This organization of data handling allows the application to function in offline mode and reduces server load.
class UserRepository(
private val remoteDataSource: UserRemoteDataSource,
private val localDataSource: UserLocalDataSource
) {
suspend fun getUsers(): List<User> {
localDataSource.getCachedUsers()?.let { return it }
val users = remoteDataSource.fetchUsers()
localDataSource.cacheUsers(users)
return users
}
}
class UserRepository {
private let remote: UserRemoteDataSource
private let local: UserLocalDataSource
func getUsers() async throws -> [User] {
if let cached = await local.getCached() { return cached }
let users = try await remote.fetch()
await local.save(users)
return users
}
}
LRU Cache (Least Recently Used) is a caching algorithm where, when the limit is reached, the element that has not been accessed for the longest time is removed. In mobile applications, LRU Cache is used for images, API responses and serialized objects. Proper data caching reduces the number of network requests and speeds up content loading. Cache in mobile applications is an essential component for high performance.
Memory Cache stores data in RAM — access is extremely fast, but capacity is limited by the application's heap size. Disk Cache saves information to the file system — it is slower but can hold more and persists between sessions. The optimal strategy in mobile development is a two-level cache: Memory Cache for hot data and Disk Cache for cold data. When working with data, the first-level cache in memory is checked first, followed by the second-level cache on disk.
class MemoryCache<K, V>(
private val maxSize: Int = 100
) {
private val cache = LinkedHashMap<K, V>(0, 0.75f, true)
fun get(key: K): V? = cache[key]
fun put(key: K, value: V) {
if (cache.size >= maxSize) {
cache.remove(cache.keys.first())
}
cache[key] = value
}
}
TTL cache (Time To Live) automatically removes an entry after a specified time interval — suitable for API data. Event-driven invalidation clears the cache when a push notification about changes is received. In mobile applications, the choice of caching strategy depends on the data type: images are cached for a long time, while a news feed requires frequent invalidation. Coil on Android and Kingfisher on iOS have already built in LRU Cache for working with images.
Offline Queue is a data structure that stores user operations (create, update, delete) in a local database when the device is offline. When the connection is restored, the Sync Manager sequentially applies these operations to the server. This type of data synchronization ensures that no change is lost during a temporary network loss. In mobile development, Offline Queue is a critical component for applications with unstable connections.
The queue is built on a table in Room or SwiftData with fields: operation type, JSON request body, timestamp and status. Sync Manager is a background service that processes pending operations, sends them to the server, updates the status and removes successful entries. Data synchronization via WorkManager on Android or BGTaskScheduler on iOS continues even after device reboot. Using Offline Queue together with proper data handling ensures a seamless user experience.
@Entity
data class SyncOperation(
@PrimaryKey val id: Long,
val endpoint: String,
val method: String,
val body: String,
val createdAt: Long
)
class SyncManager(
private val dao: SyncOperationDao,
private val api: ApiService
) {
suspend fun syncPending() {
dao.getPendingOperations().forEach { op ->
try {
api.execute(op.endpoint, op.method, op.body)
dao.delete(op.id)
} catch (e: Exception) {
// retry on next cycle
}
}
}
}
Exponential backoff between retries (1s, 2s, 4s, 8s) protects the server from thundering herd problems and prevents infinite retries. The retry limit of 5 attempts prevents queue overflow. Data synchronization in mobile applications with server-side idempotency support allows safe retries, avoiding duplicates. This is particularly important for financial transactions and orders.
Conflict Resolution is a set of strategies for situations where the same data is modified on different devices simultaneously. Basic data synchronization requires choosing an approach: Last-Write-Wins (the latest write wins), versioning (higher version wins) or manual resolution. In complex scenarios, CRDT (Conflict-Free Replicated Data Types) are used, guaranteeing mathematical convergence of data.
Last-Write-Wins is the simplest to implement but may lose user changes. Version Vector — each record stores a version number and device identifier; a conflict arises when versions do not match. CRDT is the most reliable but complex strategy: data mathematically converges to a single state without a centralized coordinator. Data synchronization in mobile applications based on CRDT is used in collaborative editing in Google Docs and Notion note synchronization.
When an application is updated, the local database structure changes: columns, tables, indexes are added. Schema Migration is the process of transforming an existing database to a new schema without data loss. Room supports migrations through the Migration class with old and new versions. SwiftData uses VersionedSchema to describe changes. Proper data synchronization between application versions requires that migrations be tested idempotently.
val migration1to2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE users ADD COLUMN avatar_url TEXT")
}
}
@Database(
entities = [User::class],
version = 2
)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
}
enum ConflictStrategy {
case lastWriteWins
case versionVector
case crdt
}
struct VersionedDocument {
let id: String
let version: Int
let data: Data
let editedBy: String
func resolve(with remote: VersionedDocument) -> VersionedDocument {
return version >= remote.version ? self : remote
}
}
Room is a Google library for local storage on Android, built on top of SQLite and providing annotations for declarative query descriptions. SwiftData is an Apple framework for iOS, macOS, watchOS and visionOS, the successor to Core Data with a concise Swift Macro syntax. Both tools solve the task of working with data on the device, but with different approaches to code organization. Cache in mobile applications is often built precisely on these technologies.
Room uses @Entity annotations for tables and @Dao for queries. DAO encapsulates all SQL operations with compile-time checking — SQL syntax errors are detected before runtime. Type Converter converts complex types (Date, List) into SQLite primitives. Modern data handling in Android applications is built around Room + Flow, providing reactive UI updates when the cache or local database changes.
SwiftData uses the @Model macro to define entities and @Query to observe data. The framework automatically tracks dependencies and updates the interface on changes. Schema migration uses VersionedSchema describing all versions. Data synchronization between SwiftData and the server is implemented through a custom Sync Manager subscribed to updates via @Query.
@Model
final class UserModel {
var id: String
var name: String
var email: String
var updatedAt: Date
init(id: String, name: String, email: String) {
self.id = id
self.name = name
self.email = email
self.updatedAt = Date()
}
}
| Criterion | Room | SwiftData |
|---|---|---|
| Platform | Android | Apple (iOS, macOS, visionOS) |
| Foundation | SQLite | SQLite (Core Data stack) |
| Syntax | Kotlin Annotations | Swift Macro |
| Migrations | Migration class | VersionedSchema |
| Reactivity | Flow / LiveData | @Query property wrapper |
| Cross-platform | Android only | Apple only |
Frequently Asked Questions
LRU Cache is a caching algorithm that, when the limit is reached, removes the least recently used item. It is used for images and API data in mobile applications.
Offline Queue saves user operations to a local database when there is no network. The Sync Manager executes them when the connection is restored, ensuring changes are delivered to the server.
Conflict Resolution is a strategy for resolving conflicts during data synchronization. Main approaches: Last-Write-Wins, Version Vector and CRDT for distributed systems.
For Android choose Room — a mature library with compile-time SQL verification. For iOS — SwiftData with declarative syntax. For cross-platform projects, SQLDelight or Realm would be suitable.
Optimal data synchronization is on every change for critical operations and background sync every 15–30 minutes for the rest. Use push notifications for instant delivery.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.