androidengineers.Book a session

Scalable Architecture

Dynamic Content Delivery & Caches

article20 minHard

Most apps display content created and stored outside the app binary: articles, images, videos, translations, configuration. Delivering this content efficiently — fast first load, offline resilience, minimal data usage — requires a deliberate delivery architecture.

Content Types and Their Delivery Needs

Content typeSizeChange frequencyCache strategy
App strings/translationsSmallRareBundle in APK; update via remote config
Configuration/feature flagsTinyModerateRemote config; short TTL
ImagesMediumRarely changes per URLLong TTL (1 year) + CDN
Articles/structured contentMediumModerateMedium TTL (1 hour) + Room cache
VideosLargeRarelyCDN + progressive streaming

CDN Strategy for Images

All user-uploaded images should be served through a CDN with image processing capabilities (Cloudflare, Imgix, Thumbor):

// Server stores: gs://bucket/uploads/abc123.jpg
// CDN serves: https://cdn.example.com/abc123.jpg?w=200&h=200&fit=crop&fmt=webp

fun buildThumbnailUrl(originalUrl: String, widthDp: Int, context: Context): String {
    val density = context.resources.displayMetrics.density
    val widthPx = (widthDp * density).toInt()
    return "$CDN_BASE${originalUrl.substringAfter("/uploads/")}?w=$widthPx&fmt=webp&q=80"
}

With a CDN, you can set Cache-Control: max-age=31536000, immutable on image responses — once loaded, the client never re-requests the image.

Remote-Controlled UI Configuration

Send layout configuration from the server to avoid shipping UI changes as app updates:

@Serializable
data class FeedConfig(
    val cardStyle: String = "standard",  // "standard", "compact", "hero"
    val showImages: Boolean = true,
    val maxItemsPerPage: Int = 20,
    val refreshIntervalMinutes: Int = 30
)

class ConfigRepository(
    private val api: ConfigApi,
    private val dataStore: DataStore<Preferences>
) {
    val feedConfig: Flow<FeedConfig> = flow {
        // 1. Emit cached config immediately
        val cached = dataStore.data.first()[FEED_CONFIG_KEY]
            ?.let { Json.decodeFromString<FeedConfig>(it) }
        if (cached != null) emit(cached)

        // 2. Fetch fresh config
        try {
            val fresh = api.getFeedConfig()
            dataStore.edit { it[FEED_CONFIG_KEY] = Json.encodeToString(fresh) }
            emit(fresh)
        } catch (e: IOException) {
            if (cached == null) emit(FeedConfig())  // emit defaults if no cache
        }
    }
}

Adaptive Content Delivery

Adjust content quality based on network conditions:

class AdaptiveContentLoader(private val connectivityManager: ConnectivityManager) {

    val isOnUnmetered: Boolean
        get() {
            val network = connectivityManager.activeNetwork ?: return false
            val caps = connectivityManager.getNetworkCapabilities(network) ?: return false
            return caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
        }

    fun buildImageUrl(imageId: String): String {
        val quality = if (isOnUnmetered) 90 else 60
        val format = "webp"
        return "$CDN_BASE/$imageId?q=$quality&fmt=$format"
    }

    fun shouldPreloadVideos(): Boolean = isOnUnmetered
}

In-App Update Delivery

Deliver major content updates while the user is active:

class ContentUpdateManager(private val appUpdateManager: AppUpdateManager) {

    fun checkForFlexibleUpdate(activity: Activity) {
        appUpdateManager.appUpdateInfo
            .addOnSuccessListener { info ->
                if (info.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE &&
                    info.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE)) {
                    appUpdateManager.startUpdateFlowForResult(
                        info,
                        AppUpdateType.FLEXIBLE,
                        activity,
                        UPDATE_REQUEST_CODE
                    )
                }
            }
    }

    fun completeFlexibleUpdate() {
        appUpdateManager.completeUpdate()  // install downloaded update
    }
}

Prefetching Critical Content

Prefetch content the user is likely to need next:

class FeedPrefetcher(
    private val repository: ArticleRepository,
    private val imageLoader: ImageLoader
) {
    suspend fun prefetchNextPage(currentPage: Int) {
        val nextArticles = repository.getArticles(page = currentPage + 1)
        // Prefetch images into Coil's disk cache
        nextArticles.forEach { article ->
            val request = ImageRequest.Builder(context)
                .data(article.thumbnailUrl)
                .memoryCachePolicy(CachePolicy.DISABLED)  // disk only
                .diskCachePolicy(CachePolicy.ENABLED)
                .build()
            imageLoader.enqueue(request)
        }
    }
}

Key Takeaways

ConceptRule
CDN for mediaAll images through CDN; use URL params for resize/format
Long TTL for immutable assetsmax-age=31536000 for images with content-addressable URLs
Remote config for UILayout changes without app update via server config
Adaptive qualityLower quality images on metered networks
PrefetchLoad next page content while user reads current page

YOUR LEARNING JOURNEY

0 of 177 available lessons completed

Progress saved in this browser. No account needed.
Dynamic Content Delivery & Caches | Android System Design | Android Engineers