Android Nomad - #36 Mobile System Design

Blueprint for Mobile System Design Interview

Android Nomad - #36 Mobile System Design

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

  1. Which platforms are we targeting? (Mobile Tablet, Desktop, TV, CarPlay)

    hints: multiple form factors, native or hybrid app

  2. Are we building for a specific country or the world?

    hints: localization, building for scale

  3. Native vs Cross-Platform vs Multi-Platform?

    hints: modularization, shared-libraries, native or hybrid app

  4. Typical consumption?

    1. Online - Cellular/WiFi/Both

    2. Offline - Local or Push-pull model

    hints: bandwidth constraints, caching

  5. 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.


  1. Get all stories

GET /v1/feed/stories

Headers: Bearer-Token, Session-Id

Response: Stories

  1. Get story data

GET /v1/stories/<id>

Headers: Bearer-Token, Session-Id

Response: StoryItem

  1. Post a like to a story

POST /v1/stories/<id>/like

Headers: Bearer-Token, Session-Id

  1. Post a reply to a story

POST /v1/stories/<id>/reply

Headers: Bearer-Token, Session-Id

Body: Comment

  1. Get all comments for the story

GET /v1/stories/<id>/comments

Headers: Bearer-Token, Session-Id

Response: Comments

  1. 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 story
  • StoryItemDTO: Represents an item within a story (image or video)
  • UserDTO: Represents a user
  • LikeDTO: Represents a like on a story
  • CommentDTO: 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 items
  • LikeEntity, CommentEntity: Room entities for likes and comments
  • StoryDao: 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 operations
  • LikeUseCase: Handles like-related operations
  • CommentUseCase: 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 screen
  • StoryViewerViewModel: ViewModel for the story viewer screen
  • StoryFeedFragment: Fragment for displaying the feed of available stories
  • StoryViewerFragment: Fragment for viewing individual stories
  • CommentBottomSheetFragment: 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

  1. 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

  2. Viewing Individual Story:

    • User taps a story, navigating to StoryViewerFragment

    • StoryViewerViewModel.loadStory(storyId) is called

    • Story details and comments are fetched and displayed

  3. 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

  4. Commenting on a Story:

    • User enters a comment and submits

    • StoryViewerViewModel.addComment(storyId, content) is called

    • CommentUseCase processes the comment through the Repository and Network layer

    • Comments are refreshed and UI updates to show the new comment

Additional Considerations

  1. Pagination: Implement pagination for story feed and comments to handle large datasets efficiently
  2. Caching: Use Room database to cache stories, likes, and comments for offline access and faster loading
  3. Real-time Updates: Consider using WebSockets or push notifications for real-time updates on likes and comments
  4. Error Handling: Implement robust error handling and provide appropriate user feedback
  5. Animations: Add smooth transitions and animations for a more engaging user experience
  6. Performance Optimization: Use efficient loading techniques for images and videos in stories
  7. Privacy and Expiration: Ensure stories are automatically removed from the feed when they expire
  8. 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 :)

Subscribe to Sid Pillai

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe