Eventual consistency means the system will become consistent — eventually. The question for mobile UX is: what does the user see in the meantime, and how do you make the inconsistent state feel intentional rather than broken?
The Core UX Problem
When a user performs an action (like a post, a like, an order), there's a window where:
- Their local state shows the result
- The server may not have processed it yet
- Other devices won't see it yet
Poorly handled, this creates ghost states: a like button that flips back, a deleted item that reappears, a sent message that disappears from history.
Optimistic UI
Show the result immediately, sync in background. The fastest and most satisfying UX when the operation is likely to succeed.
class FeedViewModel(private val repository: FeedRepository) : ViewModel() {
private val _posts = MutableStateFlow<List<Post>>(emptyList())
val posts: StateFlow<List<Post>> = _posts.asStateFlow()
fun likePost(postId: String) {
// 1. Update UI immediately
_posts.update { posts ->
posts.map { post ->
if (post.id == postId) post.copy(
isLiked = !post.isLiked,
likeCount = if (post.isLiked) post.likeCount - 1 else post.likeCount + 1
) else post
}
}
// 2. Sync to server in background
viewModelScope.launch {
val success = repository.toggleLike(postId)
if (!success) {
// 3. Roll back on failure
_posts.update { posts ->
posts.map { post ->
if (post.id == postId) post.copy(
isLiked = !post.isLiked,
likeCount = if (post.isLiked) post.likeCount + 1 else post.likeCount - 1
) else post
}
}
showError("Couldn't update like — check your connection")
}
}
}
}
Pending State Indicators
When you can't confirm success immediately, show pending state instead of either "done" or "error":
data class Post(
val id: String,
val content: String,
val syncState: SyncState = SyncState.SYNCED
)
enum class SyncState { SYNCED, PENDING, FAILED }
<!-- In item layout: show a subtle sync indicator -->
<ProgressBar
android:id="@+id/syncIndicator"
android:visibility="@{post.syncState == SyncState.PENDING ? View.VISIBLE : View.GONE}" />
<ImageView
android:id="@+id/failureIndicator"
android:src="@drawable/ic_sync_error"
android:visibility="@{post.syncState == SyncState.FAILED ? View.VISIBLE : View.GONE}" />
Handling Failure States
Design the failure path as carefully as the success path:
| Failure | UX Response |
|---|---|
| Temporary network error | Show pending indicator; retry silently |
| Persistent failure | Show error badge; offer manual retry button |
| Conflict | Show both versions; ask user to resolve |
| Server rejection (validation) | Roll back optimistic update; explain reason |
// Don't show errors for transient failures — retry silently
fun handleSyncResult(result: SyncResult, postId: String) {
when (result) {
is SyncResult.Success -> markSynced(postId)
is SyncResult.Temporary -> scheduleRetry(postId) // no user-visible error
is SyncResult.Permanent -> {
markFailed(postId)
showRetryOption(postId) // user-visible: retry button
}
}
}
Showing Pending Operations in Lists
// In adapter/Compose: render pending items differently
@Composable
fun PostItem(post: Post) {
Box {
PostContent(post)
if (post.syncState == SyncState.PENDING) {
// Subtle overlay, don't block interactions
LinearProgressIndicator(
modifier = Modifier.fillMaxWidth().align(Alignment.BottomCenter),
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.5f)
)
}
if (post.syncState == SyncState.FAILED) {
Icon(
imageVector = Icons.Default.SyncProblem,
contentDescription = "Sync failed",
modifier = Modifier.align(Alignment.TopEnd).padding(8.dp),
tint = MaterialTheme.colorScheme.error
)
}
}
}
UX Principles for Eventual Consistency
- Never show a blank screen while waiting for sync — serve from local cache immediately
- Optimistic updates for reversible actions (likes, toggles, preferences) — rollback on failure
- Pending state for irreversible actions (payments, destructive deletes) — confirm before committing
- Silent retry for transient failures — only surface the error after N retries
- Preserve user's intent — queue offline writes; don't discard them silently
- Conflict resolution UI — don't pick a winner without asking the user for ambiguous conflicts
Key Takeaways
Eventual consistency isn't a problem to hide from users — it's a state to communicate clearly. The best apps make it feel fast (optimistic updates), trustworthy (pending indicators that resolve), and recoverable (retry options that work).