Simulate a full 45-minute system design interview for a Twitter-like app. Use the four-phase framework: Requirements → High-level design → Deep dive → Trade-offs.
The Prompt
"Design the Android client for a Twitter-like microblogging app. Users can compose tweets (text + media), view a home timeline, search for hashtags, and receive notifications for likes and mentions."
Phase 1: Requirements Clarification (5 min)
Work through these questions out loud:
Functional:
Q: Does the timeline need to work offline?
A: Yes — cached timeline should be readable; composition should queue offline.
Q: Real-time or eventual consistency for likes/retweets?
A: Eventual consistency; we don't need sub-second updates.
Q: Are we designing the full app or focusing on specific flows?
A: Focus on home timeline + composition; mention search and notifications at high level.
Q: Scale?
A: 50M daily active users; 500 tweets/second peak.
Non-functional:
- Work on 3G+ networks
- Cold start < 2 seconds (P95)
- Crash-free rate > 99.5%
- Offline timeline available for last 50 tweets
Scope statement (say this aloud): "I'll focus on the home timeline feed with offline support, tweet composition with media upload, and optimistic like/retweet. I'll cover search and notifications at a higher level. I'm treating the backend as a given — I'll design the Android client architecture."
Phase 2: High-Level Design (10 min)
Draw this architecture:
┌─────────────────────────────────────────────────┐
│ UI (Compose) │
│ HomeScreen │ ComposeScreen │ TweetDetailScreen │
├─────────────────────────────────────────────────┤
│ ViewModels (MVVM + UDF) │
│ TimelineViewModel │ ComposeViewModel │
├─────────────────────────────────────────────────┤
│ Domain Layer │
│ GetTimelineUseCase │ PostTweetUseCase │
│ LikePostUseCase │ UploadMediaUseCase │
├─────────────────────────────────────────────────┤
│ Data Layer │
│ TimelineRepository (remote + local) │
│ ┌──────────────────┐ ┌─────────────────────┐ │
│ │ TwitterApi │ │ Room Database │ │
│ │ (Retrofit/OkHttp)│ │ TweetDao, DraftDao │ │
│ └──────────────────┘ └─────────────────────┘ │
├─────────────────────────────────────────────────┤
│ Infrastructure │
│ Hilt DI │ WorkManager │ FCM │ Coil │ Paging 3 │
└─────────────────────────────────────────────────┘
Key data flows to mention:
- Launch → Room cache → immediate display → background API sync → update
- Like → optimistic toggle → API call → rollback on failure
- Compose → local draft save → media upload via WorkManager → post tweet
- FCM →
onMessageReceived→ show notification + Room update
Phase 3: Deep Dive — Two Components (15 min)
Deep Dive 1: Offline-First Timeline (7 min)
// PagingSource reads from Room (always)
class TimelinePagingSource(private val dao: TweetDao) : PagingSource<Int, TweetEntity>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, TweetEntity> {
val page = params.key ?: 0
val tweets = dao.getPage(limit = params.loadSize, offset = page * params.loadSize)
return LoadResult.Page(
data = tweets,
prevKey = if (page == 0) null else page - 1,
nextKey = if (tweets.isEmpty()) null else page + 1
)
}
override fun getRefreshKey(state: PagingState<Int, TweetEntity>): Int? = null
}
// RemoteMediator syncs from network to Room
class TimelineRemoteMediator(private val api: TwitterApi, private val dao: TweetDao)
: RemoteMediator<Int, TweetEntity>() {
override suspend fun load(loadType: LoadType, state: PagingState<Int, TweetEntity>): MediatorResult {
return try {
val cursor = if (loadType == LoadType.APPEND) dao.getOldestCursor() else null
val page = api.getTimeline(cursor = cursor, count = state.config.pageSize)
dao.transaction {
if (loadType == LoadType.REFRESH) dao.clearAll()
dao.insertAll(page.tweets.map { it.toEntity() })
}
MediatorResult.Success(endOfPaginationReached = page.nextCursor == null)
} catch (e: IOException) {
MediatorResult.Error(e)
}
}
}
Failure handling: If network fails on refresh, Room cache is shown. If Room is empty and network fails, show error state with retry button.
Deep Dive 2: Optimistic Like with Rollback (8 min)
fun onLikeTapped(tweetId: String, currentlyLiked: Boolean) {
// 1. Immediate UI update
_likeState[tweetId] = !currentlyLiked
// 2. Cancel previous request (debounce)
pendingLikes[tweetId]?.cancel()
pendingLikes[tweetId] = viewModelScope.launch {
try {
if (currentlyLiked) api.unlike(tweetId) else api.like(tweetId)
// 3. Persist to Room on success
dao.updateLikeStatus(tweetId, !currentlyLiked)
} catch (e: Exception) {
// 4. Rollback
_likeState[tweetId] = currentlyLiked
_effects.emit(UiEffect.ShowError("Couldn't update like"))
}
}
}
Phase 4: Trade-offs (10 min)
State three major decisions with their trade-offs:
Decision 1: Room + RemoteMediator vs Pure API
| Room + RemoteMediator | Pure API | |
|---|---|---|
| Cold launch | Instant (from cache) | Blank screen while fetching |
| Offline | Works (shows cache) | Broken |
| Complexity | Higher (migration, sync logic) | Simpler |
| Stale data | Possible (cache TTL needed) | Always fresh |
Chosen: Room + RemoteMediator. Offline-first is a requirement; instant cold launch matters for DAU.
Decision 2: WorkManager vs foreground service for uploads
WorkManager: system manages lifecycle; runs in background; survives process death; retries automatically.
Foreground service: more control; faster start; user-visible; harder to manage lifecycle.
Chosen: WorkManager with setForeground(). Gives resilience without managing foreground service lifecycle manually.
Decision 3: WebSocket vs polling for "new tweets" indicator
WebSocket: real-time; battery impact from persistent connection.
Polling (60s): ~1 minute delay; predictable battery use.
Chosen: 60s polling. 1-minute latency is acceptable for a "new tweets" indicator. Save WebSocket budget for real-time messaging.
Self-Assessment Rubric
Requirements (20%)
[ ] Asked about offline support
[ ] Clarified scale
[ ] Scoped the problem ("I'll focus on X")
Architecture (30%)
[ ] Drew layered diagram (UI → Domain → Data)
[ ] Named specific libraries (Paging 3, Room, Hilt, WorkManager)
[ ] Showed data flow, not just boxes
Deep Dive (30%)
[ ] Code-level detail for at least one component
[ ] Handled failure cases (network error, rollback)
[ ] Specific API names (RemoteMediator, PagingSource, CoroutineWorker)
Trade-offs (20%)
[ ] At least 2 decisions with alternatives stated
[ ] Named concrete pros/cons, not vague "it's faster"
[ ] Showed awareness of the trade-off (not just "this is obviously better")