androidengineers.Book a session

Case Study: Design Twitter/X + Interview Framework

Exercise: 45-min Whiteboard Design

exercise45 minHard

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:

  1. Launch → Room cache → immediate display → background API sync → update
  2. Like → optimistic toggle → API call → rollback on failure
  3. Compose → local draft save → media upload via WorkManager → post tweet
  4. 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 + RemoteMediatorPure API
Cold launchInstant (from cache)Blank screen while fetching
OfflineWorks (shows cache)Broken
ComplexityHigher (migration, sync logic)Simpler
Stale dataPossible (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")

YOUR LEARNING JOURNEY

0 of 177 available lessons completed

Progress saved in this browser. No account needed.
Exercise: 45-min Whiteboard Design | Android System Design | Android Engineers