androidengineers.Book a session

Case Study: Design Instagram

Stories Architecture & Preloading

article20 minHard

Stories (Instagram, WhatsApp) are full-screen, time-limited media with a progress bar. The key UX quality signal is zero-wait time — every story must be ready before the user taps to it.

Data Model

data class Story(
    val id: String,
    val userId: String,
    val items: List<StoryItem>,
    val seenUpTo: Int = 0       // index of last seen item
)

data class StoryItem(
    val id: String,
    val type: StoryItemType,    // IMAGE or VIDEO
    val mediaUrl: String,
    val thumbnailUrl: String,
    val duration: Int,          // display duration in ms (5000 for images, actual length for videos)
    val seen: Boolean = false
)

enum class StoryItemType { IMAGE, VIDEO }

Progress Bar

@Composable
fun StoryProgressBar(
    itemCount: Int,
    currentIndex: Int,
    progress: Float,              // 0.0 - 1.0 for current item
    modifier: Modifier = Modifier
) {
    Row(modifier = modifier, horizontalArrangement = Arrangement.spacedBy(4.dp)) {
        repeat(itemCount) { index ->
            val segmentProgress = when {
                index < currentIndex -> 1f   // completed
                index == currentIndex -> progress  // current (animating)
                else -> 0f               // not yet reached
            }
            LinearProgressIndicator(
                progress = segmentProgress,
                modifier = Modifier.weight(1f).height(2.dp),
                color = Color.White,
                trackColor = Color.White.copy(alpha = 0.4f)
            )
        }
    }
}

Auto-Advance Timer

@HiltViewModel
class StoryViewModel @Inject constructor(
    private val repository: StoryRepository
) : ViewModel() {

    private val _currentIndex = MutableStateFlow(0)
    val currentIndex: StateFlow<Int> = _currentIndex

    private val _progress = MutableStateFlow(0f)
    val progress: StateFlow<Float> = _progress

    private var timerJob: Job? = null

    fun startAutoAdvance(duration: Int) {
        timerJob?.cancel()
        _progress.value = 0f
        timerJob = viewModelScope.launch {
            val startTime = System.currentTimeMillis()
            while (isActive) {
                val elapsed = System.currentTimeMillis() - startTime
                _progress.value = (elapsed.toFloat() / duration).coerceIn(0f, 1f)
                if (elapsed >= duration) {
                    advanceToNext()
                    break
                }
                delay(16)  // 60fps update
            }
        }
    }

    fun onPause() { timerJob?.cancel() }
    fun onResume(remainingMs: Int) { startAutoAdvance(remainingMs) }

    fun advanceToNext() {
        val next = _currentIndex.value + 1
        if (next < currentStory.items.size) {
            _currentIndex.value = next
            _progress.value = 0f
            startAutoAdvance(currentStory.items[next].duration)
        } else {
            // Move to next user's story
            nextUserStory()
        }
    }

    fun goToPrevious() {
        if (_currentIndex.value > 0) {
            _currentIndex.value -= 1
            startAutoAdvance(currentStory.items[_currentIndex.value].duration)
        }
    }
}

Preloading Strategy

The key to instant story transitions is preloading the next story's first item while the current one plays.

class StoryPreloader @Inject constructor(
    private val imageLoader: ImageLoader,     // Coil
    private val playerPool: ExoPlayerPool
) {
    fun preloadStory(story: Story) {
        val firstItem = story.items.firstOrNull() ?: return

        when (firstItem.type) {
            StoryItemType.IMAGE -> {
                // Preload image into Coil memory cache
                val request = ImageRequest.Builder(context)
                    .data(firstItem.mediaUrl)
                    .memoryCachePolicy(CachePolicy.ENABLED)
                    .build()
                imageLoader.enqueue(request)
            }
            StoryItemType.VIDEO -> {
                // Prime an ExoPlayer with the video URL so buffering starts
                val player = playerPool.acquire()
                player.setMediaItem(MediaItem.fromUri(firstItem.mediaUrl))
                player.prepare()  // starts buffering but doesn't play
                playerPool.releaseToPreload(story.id, player)
            }
        }
    }

    fun preloadAdjacentStories(stories: List<Story>, currentIndex: Int) {
        // Preload next 2 stories
        (currentIndex + 1..currentIndex + 2)
            .filter { it < stories.size }
            .forEach { preloadStory(stories[it]) }
    }
}

ExoPlayer Pool for Video Stories

class ExoPlayerPool(private val context: Context, private val maxSize: Int = 3) {
    private val available = ArrayDeque<ExoPlayer>()
    private val preloaded = mutableMapOf<String, ExoPlayer>()

    fun acquire(storyId: String? = null): ExoPlayer {
        // Return preloaded player if available
        storyId?.let { preloaded.remove(it) }?.let { return it }

        return available.removeFirstOrNull() ?: ExoPlayer.Builder(context)
            .setLoadControl(DefaultLoadControl.Builder()
                .setBufferDurationsMs(5000, 20000, 1500, 2000)
                .build())
            .build()
    }

    fun release(player: ExoPlayer) {
        player.stop()
        player.clearMediaItems()
        if (available.size < maxSize) {
            available.addLast(player)
        } else {
            player.release()
        }
    }
}

Key Takeaways

PatternRule
Progress bar segmentsOne LinearProgressIndicator per story item; 2dp height; white
16ms timer updates60fps progress animation; cancel on pause
Auto-advanceMove to next item when elapsed >= duration; use elapsed time not countdown
Preload next 2 storiesStart image load and video buffering while current story plays
ExoPlayer poolReuse players; max 3; prime with prepare() before use
Touch left/rightTap left third → go back; tap right two thirds → advance

YOUR LEARNING JOURNEY

0 of 177 available lessons completed

Progress saved in this browser. No account needed.
Stories Architecture & Preloading | Android System Design | Android Engineers