Android Nomad - #36 Mobile System Design
Blueprint for Mobile System Design Interview
In my previous System Design post, I covered how to approach a system design interview from an end to end system standpoint, a lot of readers reached out to me to create one similar for mobile system design a.k.a. client app design. This is applicable for both iOS and Android engineers.
Now let us dive into a problem.
Problem: Design Instagram Stories
In a typical 45-min interview, you are expected to cover the following:
Before you dive into the problem, understand for whom you’re building for using what. Very important step!
Step 0: Clarifying Questions
Which platforms are we targeting? (Mobile Tablet, Desktop, TV, CarPlay)
hints: multiple form factors, native or hybrid app
Are we building for a specific country or the world?
hints: localization, building for scale
Native vs Cross-Platform vs Multi-Platform?
hints: modularization, shared-libraries, native or hybrid app
Typical consumption?
Online - Cellular/WiFi/Both
Offline - Local or Push-pull model
hints: bandwidth constraints, caching
Is this application public or gated?
hints: may require authentication, token management
Step 1: Functional Requirements
Outline critical features that we intend to build. Only focus on the important ones such as:
- Display list of stories
- User can view a Story
- User can like the Story
- User can reply to the Story (ask whether nested or not)
Step 2: Non-Functional Requirements
List features that are not critical pertaining to the features that we intend to build.
Prefetch data
Support offline
Low Latency for notifications
Authentication
Pagination
Security
Session Management - user info, token, expiration
Step 3: Client Architecture
a. API Contract
Briefly mention all the APIs, the following section is purely for a reference, no need to write down anything unless asked so.
For interview mention Key Components and proceed with API Contract.
- Get all stories
GET /v1/feed/stories
Headers: Bearer-Token, Session-Id
Response: Stories
- Get story data
GET /v1/stories/<id>
Headers: Bearer-Token, Session-Id
Response: StoryItem
- Post a like to a story
POST /v1/stories/<id>/like
Headers: Bearer-Token, Session-Id
- Post a reply to a story
POST /v1/stories/<id>/reply
Headers: Bearer-Token, Session-Id
Body: Comment
- Get all comments for the story
GET /v1/stories/<id>/comments
Headers: Bearer-Token, Session-Id
Response: Comments
- Post a comment within a comment in a story
POST /v1/stories/<id>/comments/<id>/reply
Headers: Bearer-Token, Session-Id
Body: Comment
Key Components:
StoryDTO: Represents a storyStoryItemDTO: Represents an item within a story (image or video)UserDTO: Represents a userLikeDTO: Represents a like on a storyCommentDTO: Represents a comment on a story
data class StoryDTO(
val id: String,
val userId: String,
val username: String,
val items: List<StoryItemDTO>,
val createdAt: String,
val expiresAt: String,
val likeCount: Int,
val commentCount: Int,
val viewCount: Int
)
data class StoryItemDTO(
val id: String,
val type: String, // "image" or "video"
val url: String,
val duration: Int // in seconds
)
data class LikeDTO(
val id: String,
val userId: String,
val storyId: String,
val createdAt: String
)
data class CommentDTO(
val id: String,
val userId: String,
val storyId: String,
val content: String,
val createdAt: String
)b. Network
Before you suggest the network architecture, it is important to understand the nature of the communication your system expects based on the complexities
The service could be of the following types:
- REST - Use for fixed schema
- GraphQL - Use for complex queries with dynamic schema
- gRPC - Becoming more common due to its ease of use.
Response:
- JSON
- Protocol-Buffers - default in gRPC
Note: Use REST when clients and services are loosely coupled and use gRPC when clients and services are tightly coupled.
Since, this is a simple use case, you can suggest REST + JSON or Protocol Buffers
interface StoryApiService {
@GET("stories/feed")
suspend fun getStoriesFeed(): List<StoryDTO>
@GET("stories/{storyId}")
suspend fun getStory(@Path("storyId") storyId: String): StoryDTO
@POST("stories/{storyId}/like")
suspend fun likeStory(@Path("storyId") storyId: String): LikeDTO
@POST("stories/{storyId}/comment")
suspend fun commentOnStory(@Path("storyId") storyId: String, @Body content: String): CommentDTO
@GET("stories/{storyId}/comments")
suspend fun getStoryComments(@Path("storyId") storyId: String): List<CommentDTO>
}c. Data Layer
Key Components:
StoryEntity,StoryItemEntity: Room entities for stories and story itemsLikeEntity,CommentEntity: Room entities for likes and commentsStoryDao: Data Access Object for stories
@Entity(tableName = "stories")
data class StoryEntity(
@PrimaryKey val id: String,
val userId: String,
val username: String,
val createdAt: String,
val expiresAt: String,
val likeCount: Int,
val commentCount: Int,
val viewCount: Int
)
@Entity(tableName = "likes")
data class LikeEntity(
@PrimaryKey val id: String,
val userId: String,
val storyId: String,
val createdAt: String
)
@Entity(tableName = "comments")
data class CommentEntity(
@PrimaryKey val id: String,
val userId: String,
val storyId: String,
val content: String,
val createdAt: String
)
@Dao
interface StoryDao {
@Query("SELECT * FROM stories WHERE expiresAt > :currentTime ORDER BY createdAt DESC")
fun getActiveStories(currentTime: String): Flow<List<StoryEntity>>
@Query("SELECT * FROM likes WHERE storyId = :storyId AND userId = :userId")
suspend fun getUserLikeForStory(storyId: String, userId: String): LikeEntity?
@Query("SELECT * FROM comments WHERE storyId = :storyId ORDER BY createdAt DESC")
fun getStoryComments(storyId: String): Flow<List<CommentEntity>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertLike(like: LikeEntity)
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertComment(comment: CommentEntity)
}d. Business Layer
Key Components:
StoryUseCase: Contains business logic for story-related operationsLikeUseCase: Handles like-related operationsCommentUseCase: Manages comment-related operations
class StoryUseCase(private val repository: StoryRepository) {
suspend fun getStoriesFeed(): Flow<List<Story>> {
return repository.getStoriesFeed()
.map { it.map(StoryMapper::entityToDomain) }
}
suspend fun getStory(storyId: String): Story {
return repository.getStory(storyId).let(StoryMapper::entityToDomain)
}
}
class LikeUseCase(private val repository: StoryRepository) {
suspend fun likeStory(storyId: String) {
repository.likeStory(storyId)
}
suspend fun unlikeStory(storyId: String) {
repository.unlikeStory(storyId)
}
}
class CommentUseCase(private val repository: StoryRepository) {
suspend fun addComment(storyId: String, content: String) {
repository.addComment(storyId, content)
}
fun getStoryComments(storyId: String): Flow<List<Comment>> {
return repository.getStoryComments(storyId)
.map { it.map(CommentMapper::entityToDomain) }
}
}e. Presentation Layer
Key Components:
StoryFeedViewModel: ViewModel for the story feed screenStoryViewerViewModel: ViewModel for the story viewer screenStoryFeedFragment: Fragment for displaying the feed of available storiesStoryViewerFragment: Fragment for viewing individual storiesCommentBottomSheetFragment: Bottom sheet for displaying and adding comments
class StoryFeedViewModel(private val storyUseCase: StoryUseCase) : ViewModel() {
private val _stories = MutableStateFlow<List<Story>>(emptyList())
val stories: StateFlow<List<Story>> = _stories.asStateFlow()
fun loadStoriesFeed() {
viewModelScope.launch {
storyUseCase.getStoriesFeed()
.collect { _stories.value = it }
}
}
}
class StoryViewerViewModel(
private val storyUseCase: StoryUseCase,
private val likeUseCase: LikeUseCase,
private val commentUseCase: CommentUseCase
) : ViewModel() {
private val _currentStory = MutableStateFlow<Story?>(null)
val currentStory: StateFlow<Story?> = _currentStory.asStateFlow()
private val _comments = MutableStateFlow<List<Comment>>(emptyList())
val comments: StateFlow<List<Comment>> = _comments.asStateFlow()
fun loadStory(storyId: String) {
viewModelScope.launch {
_currentStory.value = storyUseCase.getStory(storyId)
commentUseCase.getStoryComments(storyId)
.collect { _comments.value = it }
}
}
fun likeStory(storyId: String) {
viewModelScope.launch {
likeUseCase.likeStory(storyId)
// Update the current story to reflect the new like count
_currentStory.value = _currentStory.value?.copy(likeCount = _currentStory.value?.likeCount?.plus(1) ?: 1)
}
}
fun addComment(storyId: String, content: String) {
viewModelScope.launch {
commentUseCase.addComment(storyId, content)
// Refresh comments after adding a new one
loadStory(storyId)
}
}
}Data Flow
Viewing Stories in Feed:
User opens the app, triggering
StoryFeedViewModel.loadStoriesFeed()Data flows through StoryUseCase, Repository, and Network layer
UI updates to display the stories feed
Viewing Individual Story:
User taps a story, navigating to StoryViewerFragment
StoryViewerViewModel.loadStory(storyId)is calledStory details and comments are fetched and displayed
Liking a Story:
User taps like button, calling
StoryViewerViewModel.likeStory(storyId)LikeUseCase processes the like through the Repository and Network layer
UI updates to reflect the new like count
Commenting on a Story:
User enters a comment and submits
StoryViewerViewModel.addComment(storyId, content)is calledCommentUseCase processes the comment through the Repository and Network layer
Comments are refreshed and UI updates to show the new comment
Additional Considerations
- Pagination: Implement pagination for story feed and comments to handle large datasets efficiently
- Caching: Use Room database to cache stories, likes, and comments for offline access and faster loading
- Real-time Updates: Consider using WebSockets or push notifications for real-time updates on likes and comments
- Error Handling: Implement robust error handling and provide appropriate user feedback
- Animations: Add smooth transitions and animations for a more engaging user experience
- Performance Optimization: Use efficient loading techniques for images and videos in stories
- Privacy and Expiration: Ensure stories are automatically removed from the feed when they expire
- Analytics: Implement analytics to track user engagement with stories, including views, likes, and comments
This is a long post hopefully it sets the right expectation for the interview. Good luck :)