androidengineers.Book a session

Case Study: Design Twitter/X + Interview Framework

Trends/Hashtags & Search

article20 minMedium

Trends, hashtag pages, and search results are high-read, moderately-stale data. They don't need real-time freshness — caching aggressively with short TTLs balances freshness and performance.

Trending Topics

data class Trend(
    val displayName: String,           // "Android"
    val query: String,                 // "#Android" or "Android"
    val tweetVolume: Long?,            // null if < threshold
    val category: TrendCategory,       // NEWS, SPORTS, ENTERTAINMENT, TECHNOLOGY
    val woeid: Int                     // Where On Earth ID — location-based
)

enum class TrendCategory { NEWS, SPORTS, ENTERTAINMENT, TECHNOLOGY, GENERAL }

// Fetch trends for user's location — cached for 5 minutes
class TrendsRepository(
    private val api: TrendsApi,
    private val cache: TrendsCache
) {
    suspend fun getTrends(woeid: Int): List<Trend> {
        val cached = cache.get(woeid)
        if (cached != null && !cached.isStale(ttlMs = 5 * 60 * 1000)) return cached.trends

        val fresh = api.getTrends(woeid)
        cache.set(woeid, CachedTrends(trends = fresh, fetchedAt = System.currentTimeMillis()))
        return fresh
    }
}

Trending UI

@Composable
fun TrendsSection(trends: List<Trend>, onTrendTapped: (Trend) -> Unit) {
    Column {
        Text("Trending", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)

        trends.take(10).forEachIndexed { index, trend ->
            Row(
                modifier = Modifier
                    .fillMaxWidth()
                    .clickable { onTrendTapped(trend) }
                    .padding(horizontal = 16.dp, vertical = 12.dp),
                horizontalArrangement = Arrangement.SpaceBetween
            ) {
                Column(Modifier.weight(1f)) {
                    Text(
                        trend.category.name.lowercase().replaceFirstChar { it.uppercase() },
                        style = MaterialTheme.typography.labelSmall,
                        color = MaterialTheme.colorScheme.onSurfaceVariant
                    )
                    Text(trend.displayName, style = MaterialTheme.typography.bodyLarge, fontWeight = FontWeight.Medium)
                    trend.tweetVolume?.let {
                        Text(
                            "${formatVolume(it)} tweets",
                            style = MaterialTheme.typography.bodySmall,
                            color = MaterialTheme.colorScheme.onSurfaceVariant
                        )
                    }
                }
                Text("${index + 1}", color = MaterialTheme.colorScheme.onSurfaceVariant)
            }
            Divider()
        }
    }
}

private fun formatVolume(volume: Long): String = when {
    volume >= 1_000_000 -> "${volume / 1_000_000}M"
    volume >= 1_000 -> "${volume / 1_000}K"
    else -> volume.toString()
}

Hashtag Page

data class HashtagPage(
    val hashtag: String,
    val tweetCount: Long,
    val topTweets: List<Tweet>,      // curated by engagement
    val recentTweets: PagingData<Tweet>  // chronological; Paging 3
)

@HiltViewModel
class HashtagViewModel @Inject constructor(
    savedStateHandle: SavedStateHandle,
    private val api: HashtagApi
) : ViewModel() {
    private val hashtag = savedStateHandle.get<String>("hashtag") ?: ""

    val topTweets: StateFlow<List<Tweet>> = flow {
        emit(api.getTopTweets(hashtag, limit = 5))
    }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())

    val recentTweets: Flow<PagingData<Tweet>> = Pager(
        config = PagingConfig(pageSize = 20),
        pagingSourceFactory = { HashtagPagingSource(api, hashtag) }
    ).flow.cachedIn(viewModelScope)
}

Search

@HiltViewModel
class SearchViewModel @Inject constructor(
    private val api: SearchApi
) : ViewModel() {

    private val _query = MutableStateFlow("")
    val query: StateFlow<String> = _query

    val suggestions: StateFlow<List<SearchSuggestion>> = _query
        .debounce(200)
        .filter { it.isNotBlank() }
        .flatMapLatest { q ->
            flow {
                emit(api.getSuggestions(q))
            }.catch { emit(emptyList()) }
        }
        .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())

    // Paging source for full search results
    fun search(query: String): Flow<PagingData<Tweet>> = Pager(
        config = PagingConfig(pageSize = 20),
        pagingSourceFactory = { SearchPagingSource(api, query) }
    ).flow.cachedIn(viewModelScope)
}

data class SearchSuggestion(
    val type: SuggestionType,
    val displayText: String,
    val query: String
)

enum class SuggestionType { HASHTAG, USER, SAVED_SEARCH }

@Composable
fun SearchSuggestions(suggestions: List<SearchSuggestion>, onTap: (SearchSuggestion) -> Unit) {
    LazyColumn {
        items(suggestions) { suggestion ->
            Row(
                modifier = Modifier.fillMaxWidth().clickable { onTap(suggestion) }.padding(16.dp)
            ) {
                Icon(
                    imageVector = when (suggestion.type) {
                        SuggestionType.HASHTAG -> Icons.Default.Tag
                        SuggestionType.USER -> Icons.Default.Person
                        SuggestionType.SAVED_SEARCH -> Icons.Default.History
                    },
                    contentDescription = null,
                    modifier = Modifier.padding(end = 12.dp)
                )
                Text(suggestion.displayText, style = MaterialTheme.typography.bodyLarge)
            }
        }
    }
}

Key Takeaways

PatternRule
Trends cache TTL5 minutes; trends change hourly — no need for realtime
WOEID locationFetch trends for user's nearest city; fallback to worldwide
Hashtag page layoutTop tweets (engagement-ranked) above recent tweets (chronological)
Search debounce200ms — faster than tweet composition (300ms); search feels more responsive
Search suggestionsLocal history + server autocomplete; show before user finishes typing
PaginationAll search results use PagingSource + cursor

YOUR LEARNING JOURNEY

0 of 177 available lessons completed

Progress saved in this browser. No account needed.
Trends/Hashtags & Search | Android System Design | Android Engineers