1. Overview
This document extends the LLD of Instagram Stories by mapping it into Android system design. It covers the client-side architecture (activities, fragments, services, view models), data handling (Room/DB, network layer, repositories), media handling, caching, lifecycle awareness, and background tasks. Includes sample Kotlin code to illustrate usage.
2. Android App Architecture
Adopt MVVM + Clean Architecture layered pattern:
- UI Layer: Activities, Fragments, Custom Views (StoriesContainer, StoryPlayer, OverlayRenderer, ProgressBar).
- ViewModel Layer: Exposes LiveData/StateFlow for story lists, playback states, replies, etc.
- Repository Layer: Handles data orchestration between remote APIs, local DB, and cache.
- Data Layer: Retrofit/OkHttp for APIs, Room for local storage, MediaCache for CDN media.
3. Core Android Components
3.1 UI Components
- StoriesActivity: entry point for viewing stories, hosts fragments
- StoryTimelineFragment: displays story list for a given user/followees.
- StoryPlayerFragment: plays one story; uses ExoPlayer for videos, Glide/Coil for images.
- OverlayRendererView: custom view for text, stickers, polls, gestures.
- ProgressBarView: custom progress indicator.
3.2 ViewModels
- StoriesViewModel: loads timeline stories, prefetches media.
- StoryPlayerViewModel: manages play/pause/seek, lifecycle-safe ExoPlayer.
- RepliesViewModel: handles replies, emoji reactions, and posting.
3.3 Data Layer
- RemoteDataSource: Retrofit APIs for Stories Service.
- LocalDataSource: Room DB for caching metadata & recent viewers.
- Repository: merges data sources; exposes Kotlin Flow/LiveData to ViewModels.
3.4 Background/Workers
- WorkManager: for offline posting of stories, retries on upload failures.
- FCM Push Receiver: triggers updates for new stories.
- DownloadService: prefetches next N media items in timeline.

4. Android Data Models (Kotlin)
data class Story(
val storyId: String,
val userId: String,
val mediaType: MediaType,
val mediaUrl: String,
val thumbnailUrl: String?,
val duration: Int,
val createdAt: Long,
val expiresAt: Long,
val privacy: PrivacyType,
val overlay: Overlay?,
val isArchived: Boolean = false,
val isHighlighted: Boolean = false
)
enum class MediaType {
IMAGE,
VIDEO,
BOOMERANG,
REEL_PREVIEW,
TEXT
}
data class StoryReply(
val replyId: String,
val storyId: String,
val fromUserId: String,
val toUserId: String,
val message: String,
val createdAt: Long,
val isSeenByOwner: Boolean
)
5. Playback Handling (Android)
- ExoPlayer for video playback (with adaptive streaming, caching, preloading).
- Glide/Coil for image decoding and caching.
- GestureDetector for tap, swipe, and long-press.
- Lifecycle-aware playback: pause/resume with onPause()/onResume().
6. Caching & Prefetching
- DiskCache (Glide/Coil): store images, thumbnails.
- ExoPlayer Cache: progressive video caching.
- Room DB: story metadata and state (seen/unseen).
- PrefetchService: background preloading of next 2–3 stories.
7. Networking Layer
- Retrofit + OkHttp interceptors (auth, logging).
- API response mapped to Kotlin models.
- Error handling with sealed classes (Success, Error, Loading).
- Coroutines for async requests.
8. Security & Privacy (Client-Side)
- Use short-lived signed URLs; refresh tokens as needed.
- Enforce encryption (HTTPS).
- Store minimal metadata locally (no sensitive viewer lists cached).
9. Notifications
- FCM integration: receive push for new stories posted by followees.
- Deep links into StoriesActivity → StoryTimelineFragment.
- Notification channels for story updates.
10. Offline & Reliability
- Story posting with WorkManager retries.
- Local persistence of uploads until confirmed by server.
- Local cache for previously seen stories for smooth offline playback.
11. Example Flows
Posting Story (Android)
User selects media → Edit overlays → Confirm → StoriesViewModel calls Repository → Repository requests upload token (Retrofit) → WorkManager uploads media (OkHttp/Multipart) → On success, metadata stored in Room + synced with backend
Viewing Story (Android)
StoriesActivity launched → StoriesViewModel fetches timeline → Repository fetches from Room + API → StoryPlayerFragment initialized with ExoPlayer/Glide → PrefetchService starts downloading next media → View event posted via Repository → API → Analytics pipeline
12. Sample Code (Kotlin)
12.1 Retrofit API Interface
interface StoriesApiService {
@GET("stories/timeline")
suspend fun getTimeline(): List<StoryDto>
@POST("stories")
suspend fun createStory(@Body story: StoryUploadRequest): StoryDto
@POST("stories/{id}/reply")
suspend fun replyToStory(
@Path("id") storyId: String,
@Body reply: ReplyRequest
): ReplyResponse
}
12.2 Room Entity
@Entity(tableName = "stories") data class StoryEntity( @PrimaryKey val storyId: String, val userId: String, val mediaType: String, val mediaUrl: String, val createdAt: Long, val expiresAt: Long, val isSeen: Boolean = false )
12.3 DAO
@Dao
interface StoryDao {
@Query("SELECT * FROM stories WHERE expiresAt > :now ORDER BY createdAt DESC")
fun getActiveStories(now: Long): Flow<List<StoryEntity>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertStories(stories: List<StoryEntity>)
@Query("UPDATE stories SET isSeen = 1 WHERE storyId = :id")
suspend fun markAsSeen(id: String)
}
12.4 Repository
class StoriesRepository(private val api: StoriesApiService, private val dao: StoryDao) {
fun getTimelineStories(): Flow<List<StoryEntity>> = flow {
val local = dao.getActiveStories(System.currentTimeMillis())
emitAll(local)
val remote = api.getTimeline()
dao.insertStories(remote.map { it.toEntity() })
}
suspend fun markSeen(storyId: String) = dao.markAsSeen(storyId)
}
12.5 ViewModel
class StoriesViewModel(private val repository: StoriesRepository) : ViewModel() {
val stories: StateFlow<List<StoryEntity>> =
repository.getTimelineStories().stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
fun markSeen(storyId: String) {
viewModelScope.launch { repository.markSeen(storyId) }
}
}
12.6 StoryPlayerFragment (simplified)
class StoryPlayerFragment : Fragment(R.layout.fragment_story_player) {
private lateinit var exoPlayer: ExoPlayer
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
exoPlayer = ExoPlayer.Builder(requireContext()).build()
val playerView = view.findViewById<PlayerView>(R.id.storyPlayerView)
playerView.player = exoPlayer
val mediaUrl = arguments?.getString("mediaUrl") ?: return
val mediaItem = MediaItem.fromUri(mediaUrl)
exoPlayer.setMediaItem(mediaItem)
exoPlayer.prepare()
exoPlayer.playWhenReady = true
}
override fun onPause() {
super.onPause()
exoPlayer.pause()
}
override fun onDestroyView() {
super.onDestroyView()
exoPlayer.release()
}
}
13. Edge Cases
- Backgrounded app → ExoPlayer paused automatically.
- Expired story → Repository filters out based on expiresAt.
- Upload failure → WorkManager retries with exponential backoff.
- Network loss mid-view → display cached version, mark as unsynced.
14. Metrics & Monitoring (Client-Side)
- Player error rate.
- Average story load time.
- Prefetch success ratio.
- WorkManager retry success.
15. Why Room (Local DB) is Used in the Stories Feature?
This section is not really required but sure you can have discussion around this.
1. Offline Availability
Users often open Instagram in poor networks (airports, subways, etc.).
- Room stores the last fetched stories locally.
- Even without internet, you can still show cached stories in the UI.
- Once the network is back, fresh data is fetched from the API and synced.
Example: You open stories → network drops → you can still swipe through previously loaded stories because they’re cached in Room.
2. Fast Loading
- API calls (Retrofit) have latency; Room reads are near-instant.
- When the app opens, the timeline can be rendered instantly using local cache while fetching updates in the background.
“Instant feel” = better UX + higher retention.
3. Data Consistency Across Layers
Room acts as a single source of truth:
- Repository emits data from Room (
Flow<List<StoryEntity>>). - Whenever API updates arrive, you just update Room — and UI auto-refreshes via Flow/LiveData.
This pattern is known as:
Offline-First with Reactive Cache
4. Marking Story as Seen
When a user watches a story:
- Mark it as seen locally in Room immediately.
- Later, sync “seen” status to the server when possible.
If network is slow, UI still updates instantly (thanks to Room).
5. Persisting User Actions / Analytics
User replies, emoji reactions, or partial uploads can be stored temporarily in Room until background upload (via WorkManager) completes successfully.
6. Reduced Server Load
By caching stories, you don’t have to re-fetch the same data repeatedly, saving:
- API calls
- Bandwidth
- Battery
Job Offers
16. Conclusion
In this article, we explored the low-level design of Instagram Stories on Android, covering every layer from networking, data handling, caching, and playback to UI rendering and lifecycle management.
Key takeaways:
- MVVM + Clean Architecture ensures a clear separation of concerns, making the module maintainable and scalable.
- Room Database plays a critical role in providing offline support, fast loading, and reactive updates.
- ExoPlayer and Glide/Coil together enable smooth media playback with caching and prefetching.
- WorkManager and background services ensure reliability for story uploads and offline interactions.
- By mapping DTOs → Domain Models → Entities → UI, we maintain data consistency while keeping the user experience seamless.
This design balances performance, reliability, and maintainability, providing a solid foundation for implementing any real-world story feature on Android.
Whether you’re building your own stories module or studying system design patterns, these principles ensure a responsive, offline-friendly, and user-centric experience.
Any comments and feedback are welcome. This article is just an overview to help you get started with system design discussions — actual implementations may vary, and the details here are meant for learning and reference.
Connect with me on Linkedin: https://www.linkedin.com/in/ninadbhase/
This article was previously published on proandroiddev.com


