androidengineers.Book a session

Case Study: Design Instagram

Realtime Likes/Comments

article20 minHard

Likes and comments are high-frequency interactions. The gold standard UX: instant optimistic update on tap, graceful rollback on failure, and realtime increments from other users via WebSocket.

Like System: Optimistic Update

data class Post(
    val id: String,
    val authorName: String,
    val imageUrl: String,
    val likeCount: Int,
    val isLikedByMe: Boolean,
    // For display: show "You and 42 others" vs "43 likes"
)

@HiltViewModel
class PostViewModel @Inject constructor(
    private val repository: PostRepository,
    private val analytics: AnalyticsTracker
) : ViewModel() {

    private val _posts = MutableStateFlow<List<Post>>(emptyList())
    val posts: StateFlow<List<Post>> = _posts

    // In-flight like requests — prevent double-tap races
    private val pendingLikes = ConcurrentHashMap<String, Job>()

    fun onLikeTapped(postId: String) {
        // Cancel pending request for this post (debounce)
        pendingLikes[postId]?.cancel()

        val current = _posts.value.find { it.id == postId } ?: return
        val wasLiked = current.isLikedByMe

        // Optimistic update
        updatePost(postId) { post ->
            post.copy(
                isLikedByMe = !wasLiked,
                likeCount = if (wasLiked) post.likeCount - 1 else post.likeCount + 1
            )
        }

        pendingLikes[postId] = viewModelScope.launch {
            try {
                if (wasLiked) {
                    repository.unlike(postId)
                } else {
                    repository.like(postId)
                    analytics.track(AnalyticsEvent.PostLiked(postId))
                }
                pendingLikes.remove(postId)
            } catch (e: Exception) {
                // Rollback on failure
                updatePost(postId) { post ->
                    post.copy(
                        isLikedByMe = wasLiked,
                        likeCount = if (wasLiked) post.likeCount + 1 else post.likeCount - 1
                    )
                }
                // Show error snackbar
                _effects.emit(UiEffect.ShowSnackbar("Couldn't update like. Try again."))
            }
        }
    }

    private fun updatePost(postId: String, transform: (Post) -> Post) {
        _posts.value = _posts.value.map { if (it.id == postId) transform(it) else it }
    }
}

Realtime Like Count via WebSocket

class FeedRealtimeUpdater @Inject constructor(
    private val webSocket: FeedWebSocket,
    private val viewModel: PostViewModel
) {
    fun start() = viewModelScope.launch {
        webSocket.events().collect { event ->
            when (event) {
                is FeedEvent.LikeCountUpdate -> {
                    viewModel.applyServerLikeCount(event.postId, event.count, event.isLikedByViewer)
                }
                is FeedEvent.NewComment -> {
                    viewModel.applyNewComment(event.postId, event.comment)
                }
            }
        }
    }
}

// In ViewModel: accept server truth without animation
fun applyServerLikeCount(postId: String, count: Int, isLikedByViewer: Boolean) {
    updatePost(postId) { it.copy(likeCount = count, isLikedByMe = isLikedByViewer) }
}

Comment System

data class Comment(
    val id: String,
    val postId: String,
    val authorId: String,
    val authorName: String,
    val authorAvatarUrl: String,
    val text: String,
    val createdAt: Long,
    val isPending: Boolean = false  // for optimistic display
)

fun onCommentSubmit(postId: String, text: String) {
    if (text.isBlank()) return

    val tempId = "pending_${System.currentTimeMillis()}"
    val optimisticComment = Comment(
        id = tempId,
        postId = postId,
        authorId = currentUser.id,
        authorName = currentUser.name,
        authorAvatarUrl = currentUser.avatarUrl,
        text = text,
        createdAt = System.currentTimeMillis(),
        isPending = true
    )

    // Add optimistic comment immediately
    _comments.value = _comments.value + optimisticComment

    viewModelScope.launch {
        try {
            val serverComment = repository.postComment(postId, text)
            // Replace pending with server version
            _comments.value = _comments.value.map {
                if (it.id == tempId) serverComment else it
            }
        } catch (e: Exception) {
            // Remove the pending comment on failure
            _comments.value = _comments.value.filter { it.id != tempId }
            _effects.emit(UiEffect.ShowSnackbar("Comment failed. Try again."))
        }
    }
}

Like Button Animation

@Composable
fun LikeButton(isLiked: Boolean, likeCount: Int, onLike: () -> Unit) {
    val scale = remember { Animatable(1f) }
    val scope = rememberCoroutineScope()

    val color by animateColorAsState(
        targetValue = if (isLiked) Color.Red else Color.Gray,
        animationSpec = tween(200),
        label = "like_color"
    )

    Column(horizontalAlignment = Alignment.CenterHorizontally) {
        Icon(
            imageVector = if (isLiked) Icons.Filled.Favorite else Icons.Outlined.FavoriteBorder,
            contentDescription = if (isLiked) "Unlike" else "Like",
            tint = color,
            modifier = Modifier
                .scale(scale.value)
                .clickable {
                    scope.launch {
                        scale.animateTo(1.3f, spring(dampingRatio = Spring.DampingRatioMediumBouncy))
                        scale.animateTo(1f)
                    }
                    onLike()
                }
        )
        Text(
            text = likeCount.toString(),
            style = MaterialTheme.typography.labelSmall
        )
    }
}

Key Takeaways

PatternRule
Optimistic likeUpdate UI immediately; rollback on API failure
ConcurrentHashMap for pending likesCancel previous request on double-tap
Debounce double-tapsCancel old like coroutine before issuing new request
WebSocket for live countsPush count updates when other users like; reconcile with optimistic state
Pending commentsShow immediately with isPending = true; replace with server version
Bounce animationAnimatable scale 1.0 → 1.3 → 1.0 on tap; adds delight without performance cost

YOUR LEARNING JOURNEY

0 of 177 available lessons completed

Progress saved in this browser. No account needed.
Realtime Likes/Comments | Android System Design | Android Engineers