Offline-First Multi-Level Caching: Architecture for Mobile Apps

Imagine: a user opens a mobile app on the subway, where the signal drops every 30 seconds. Without caching, every screen becomes an endless spinner, and on error — a blank void. The offline-first approach solves this: data loads once when connected and remains available locally. But cache isn't a ma

Development and support of all types of mobile applications:

Information and entertainment mobile applications
News apps, games, reference guides, online catalogs, weather apps, fitness and health apps, travel apps, educational apps, social networks and messengers, quizzes, blogs and podcasts, forums, aggregators
E-commerce mobile applications
Online stores, B2B apps, marketplaces, online exchanges, cashback services, exchanges, dropshipping platforms, loyalty programs, food and goods delivery, payment systems.
Business process management mobile applications
CRM systems, ERP systems, project management, sales team tools, financial management, production management, logistics and delivery management, HR management, data monitoring systems
Electronic services mobile applications
Classified ads platforms, online schools, online cinemas, electronic service platforms, cashback platforms, video hosting, thematic portals, online booking and scheduling platforms, online trading platforms

These are just some of the types of mobile applications we work with, and each of them may have its own specific features and functionality, tailored to the specific needs and goals of the client.

Showing 1 of 1All 1734 services
Offline-First Multi-Level Caching: Architecture for Mobile Apps
Medium
~2-3 days

Our competencies:

Frequently Asked Questions

Latest works

  • image_mobile-applications_feedme_467_0.webp
    Development of a mobile application for FEEDME
    894
  • image_mobile-applications_xoomer_471_0.webp
    Development of a mobile application for XOOMER
    782
  • image_mobile-applications_rhl_428_0.webp
    Development of a mobile application for RHL
    1216
  • image_mobile-applications_zippy_411_0.webp
    Development of a mobile application for ZIPPY
    1079
  • image_mobile-applications_affhome_429_0.webp
    Development of a mobile application for Affhome
    1002
  • image_mobile-applications_flavors_409_0.webp
    Development of a mobile application for the FLAVORS company
    597

Imagine: a user opens a mobile app on the subway, where the signal drops every 30 seconds. Without caching, every screen becomes an endless spinner, and on error — a blank void. The offline-first approach solves this: data loads once when connected and remains available locally. But cache isn't a magic wand: incorrect invalidation strategy leads to stale data, and disk overflow crashes the app. We've been designing multi-level caches for years, launched over 50 projects with offline-first architecture, and know the pitfalls. Our goal is to make users see data instantly and developers never wonder why the cache isn't updating.

Designing a Multi-Level Cache

A good cache architecture has several levels:

Level Storage Speed Lifetime Tools
Memory RAM Instant App restart NSCache (iOS), LruCache (Android)
Disk Files/DB Fast Hours–days Room (Android), CoreData (iOS)
Network Server Slow Forever (source of truth) API with ETag

We use the stale-while-revalidate strategy (cache-first, refresh-in-background): first display the cache instantly, then update data in the background. If data changed, the UI redraws. This gives the user a feeling of instant performance.

Choosing the right TTL is key. For API responses we set TTL from 5 to 30 minutes, for images up to 7 days. On unstable networks, we use ETag, saving up to 80% of traffic (average server cost savings up to 70%, e.g., $10k/month for high-traffic apps). Read time from Room is 1–3 ms, typical disk cache size is 10–50 MB.

On one project (a retail chain app), we implemented a three-level cache: list headers in LruCache (Android), details in Room with a 15-minute TTL, images in Coil with a 100 MB disk cache. Result: average screen load time dropped from 3 seconds to 200 ms, and server load fell by 65%. Contact us for a consultation to select optimal TTLs for your data.

Data Type Recommended TTL Reason
Product list 5–10 minutes Frequently changes, freshness critical
Product images 24 hours–7 days Rarely change, large size
Configurations 30 minutes Rarely change, loaded on start

Implementing Offline-First API Response Cache

On Android we use Room as a persistent cache with timestamps and ETags. Example:

// Entity with timestamp for invalidation @Entity(tableName = "products_cache") data class ProductCacheEntity( @PrimaryKey val id: String, val categoryId: String, val payload: String, // JSON string val cachedAt: Long, // Unix timestamp val etag: String? = null ) // Repository: cache-first logic class ProductRepository( private val api: ProductApi, private val dao: ProductCacheDao, private val cacheMaxAge: Long = 5 * 60 * 1000L // 5 minutes ) { fun getProductsByCategory(categoryId: String): Flow<List<Product>> = flow { // 1. Emit cached data immediately val cached = dao.getByCategory(categoryId) if (cached.isNotEmpty()) { emit(cached.map { it.toProduct() }) } // 2. Check freshness val oldestEntry = cached.minOfOrNull { it.cachedAt } ?: 0L val needsRefresh = System.currentTimeMillis() - oldestEntry > cacheMaxAge if (needsRefresh || cached.isEmpty()) { try { val fresh = api.getProducts(categoryId) val entities = fresh.map { it.toCacheEntity(categoryId) } dao.upsertAll(entities) emit(fresh) } catch (e: IOException) { // Network unavailable — cache already emitted, do nothing if (cached.isEmpty()) throw e // nothing to show — propagate } } } } 

UI subscribes to the Flow and receives data twice: first cache, then fresh. This is the basis of offline-first.

ETag for Traffic Savings

Instead of time-based invalidation, you can use HTTP ETag. The server returns ETag: "v42", the next request sends If-None-Match: "v42" — if data hasn't changed, server returns 304 with no body. Traffic savings can reach 80%.

OkHttp (HTTP client for Android and React Native) supports HTTP cache out of the box:

val cache = Cache( directory = File(context.cacheDir, "http-cache"), maxSize = 10L * 1024 * 1024 // 10 MB ) val client = OkHttpClient.Builder() .cache(cache) .build() 

On iOS, URLSession works similarly through URLCache. But HTTP caching only works with correct Cache-Control headers from the server.

Image Caching: Battle-Tested Libraries

For images we use proven solutions:

  • Android: Coil — Kotlin-first, Compose-ready, memory + disk cache, placeholder/error states.
  • iOS: Kingfisher or SDWebImage — async loading, NSCache + disk, progressive JPEG.
  • React Native: react-native-fast-image (wrapper over SDWebImage/Glide).
// Coil in Compose AsyncImage( model = ImageRequest.Builder(context) .data(product.imageUrl) .memoryCacheKey(product.id) .diskCacheKey(product.imageUrl) .crossfade(true) .build(), contentDescription = product.title, placeholder = painterResource(R.drawable.placeholder), error = painterResource(R.drawable.error_image) ) 

Coil is on average 2x faster than Glide when loading images from cache (tested on Android 12). Kingfisher on iOS provides disk access times under 10 ms.

Cache Invalidation Strategies: Which to Choose?

Invalidation is the hardest part. We use a combination of approaches:

  • TTL (time-to-live) — automatic expiry after a set interval. Simple, predictable, but data ages between TTL.
  • Event-based — push notification from server on data change. Instant, but requires server support.
  • Version-based — server sends dataVersion, number comparison. Precise without data transfer.
  • Pull-to-refresh — user initiates update. Always needed as a fallback.

We combine TTL for basic data, event-based for critical data (e.g., account balance), and pull-to-refresh as a backup. Implementation takes 1–2 weeks depending on the number of data types.

How to Choose a Caching Strategy for Specific Data?

For frequently changing data (news feed), use TTL 5–10 minutes with event-based updates. For static reference data, versioning with a long TTL. Testing on real scenarios shows that the right combination of strategies improves user retention by 15–25%.

Steps to Implement Multi-Level Cache

  1. Audit current APIs and data.
  2. Design cache schema: choose layers, TTL for each type.
  3. Implement in-memory (NSCache/LruCache) and disk cache (Room/CoreData).
  4. Integrate HTTP cache and ETag.
  5. Set up invalidation (event-based + TTL).
  6. Test offline scenarios: network off, slow, drop.
  7. Documentation and team training.

Что входит в работу

  • Audit of current APIs and data
  • Design of cache architecture (layer schema, TTL selection)
  • Implementation of in-memory and disk cache
  • Integration of HTTP cache and ETag
  • Configuration of invalidation (event-based + TTL)
  • Testing offline scenarios (network off, slow, drop)
  • Documentation and team training
  • Support during App Store / Google Play release

Our team has developed over 50 mobile apps with offline-first architecture. We guarantee stable cache performance in unstable network conditions. All solutions undergo code review and load testing. Contact us for a project evaluation — we'll propose an optimal caching scheme for your data.