Google is known for asking some of the most challenging system design questions in the industry, covering distributed systems, data infrastructure, and large-scale web services. Their interviews emphasise designing systems that handle billions of users and petabytes of data.
Interview focus: Distributed systems, search infrastructure, real-time data processing, and global-scale services.
Reddit is a popular senior Android system design question at companies like Reddit, Meta, Google, and TikTok. It sits in a productive zone: approachable enough to get started quickly, but with enough depth in pagination, offline strategy, vote state management, and media handling to fill a full 45-minute interview.
In a real interview, the interviewer may focus on a specific component — like the feed flow — and expect you to go deep on architecture, pagination, image loading, and offline handling for that area specifically. This guide covers the major building blocks with the depth and balance a senior interview expects.
Step 1: Clarify the Scope
Interviewer: Design the Reddit Android app.
Candidate: A few quick questions. Are we designing the full app, or specific flows? I'm thinking home feed with infinite scroll, post detail with nested comments, voting, and subreddit browsing. Should offline access be supported? What about media types — images and videos in the feed? And are push notifications and deep linking in scope?
Interviewer: Full core experience — feed, post detail, voting, and subreddit browsing. All media types. Offline is a hard requirement. Push notifications and deep linking are in scope.
Candidate: Great. The feed architecture and vote state are where the most interesting depth lives. Let me start with requirements and numbers, then walk through each component.
Requirements
Functional
Home feed: infinite scroll of posts from subscribed subreddits
Subreddit browsing: subscribe, view feed, rules, and about
Post detail: full post with nested, collapsible comment tree
Voting: upvote/downvote on posts and comments — immediate feedback, synced in background
Media: text, images, GIFs, and video posts in the same feed
Deep linking: notification tap lands on the correct post or comment
Non-Functional
Smooth infinite scroll — no blank cells or layout shifts during pagination
Instant vote feedback — visual update in under 50ms
Offline resilience — cached feed readable without network
Memory safe — heterogeneous media across a long list must not OOM
Battery aware — media autoplay respects network type
Back-of-the-Envelope Estimates
Interviewer: What numbers are you working with?
Candidate: On the client:
plaintext
Feed page size: 25 posts per pagePost metadata: ~2 KB per postThumbnail: ~150–300 KB (CDN-served)Comment depth: Reddit caps at 8 levelsComments per post: Median ~50, popular posts ~1,000+Local Room cache: 500 posts × 2 KB = ~1 MB metadata — trivially small Images managed by Coil/Glide disk cache (~100 MB)Pagination style: Reddit API: cursor-based ("after" token) — not page numbers
Two things to note. First, pagination is cursor-based — not offset. This shapes the Paging 3 setup significantly. Second, a popular thread can have thousands of nested comments — the tree needs a specific flattening strategy. Memory is the real risk: 200 thumbnails in a long scroll session will OOM if image loading doesn't handle eviction.
Client Architecture
Interviewer: Walk me through the overall architecture.
Candidate: Clean Architecture with MVVM or MVI at the presentation layer, Paging 3 with RemoteMediator for the feed, and Room as the persistent cache.
The single most important principle: Room is the single source of truth.
Every screen observes Room via Flow. The network populates Room; Room drives the UI. This is what makes offline seamless — the UI doesn't know or care whether data came from a network response or the local cache from yesterday.
Why MVI over plain MVVM?
Reddit's feed has complex, multi-dimensional state: loading, empty, error, refreshing, and per-item vote state that changes independently of the list. MVI's single UiState object keeps all of this in one observable, preventing the inconsistencies that creep in with multiple independent StateFlow fields.
Block 1: Infinite Feed with Paging 3 and RemoteMediator
This is the centrepiece — and what interviewers probe most on Reddit-style questions.
The two Paging 3 approaches:
A plain PagingSource reads from a single source — network or local database, not both. RemoteMediator coordinates both: when the user scrolls to the end of locally cached data, it fetches the next page from the network, writes it to Room, and Room's PagingSource emits the updated data automatically. Room stays the source of truth.
Interviewer: Walk me through the feed loading flow.
Candidate:
plaintext
FIRST OPEN──────────────────────────────────────────────────Pager reads from Room (FeedPagingSource) │ ├─ Cache is fresh (< 1h old)? │ → SKIP_INITIAL_REFRESH │ Shows cached data instantly, no network wait │ └─ Cache is stale? → LAUNCH_INITIAL_REFRESH Fetches page 1 from network in background Writes to Room → PagingSource invalidates → UI updatesSCROLL TO NEAR END──────────────────────────────────────────────────Paging 3 triggers RemoteMediator.load(APPEND) │ ▼Read "after" cursor from RemoteKeys table (stored alongside posts on previous load) │ ▼GET /hot?after={cursor}&limit=25 │ ▼Database transaction: CLEAR old data (REFRESH only) / APPEND new posts INSERT posts into Room INSERT RemoteKeys (next cursor per post) │ ▼PagingSource invalidates → new rows appear at bottom
The RemoteKeys table is a critical supporting table that stores the after cursor token alongside each post. Without it, Paging 3 has no reliable anchor for which cursor to use when the user is deep in the list or after a process kill.
Cursor-based vs offset pagination:
Interviewer: Why cursor-based and not page numbers?
Candidate: Offset pagination (page=2) breaks on a live feed. If 10 new posts appear at the top between page 1 and page 2, page 2 shifts — items repeat or get skipped. A cursor anchors to the last seen item ID, so after=t3_xyz always returns the next 25 posts after that specific post, regardless of how the top of the feed has changed. This is more resilient and is what Reddit's API actually uses.
InitializeAction.SKIP_INITIAL_REFRESH is a clean optimisation worth naming explicitly. When the RemoteMediator detects fresh cached data on initialize(), it returns SKIP_INITIAL_REFRESH — skipping the network call on app open. Users on frequently-opened apps see their feed instantly from Room, with a background refresh happening silently. This is a meaningful improvement to perceived app startup time.
Handling REFRESH vs APPEND:
On REFRESH (pull-to-refresh or stale cache), clear the existing posts and remote keys before writing fresh data. On APPEND, only insert the new page — don't touch existing posts. Mixing these up causes the list to reset to the top mid-scroll.
All Room writes during a mediator load are wrapped in a withTransaction block. If any write fails, none of them persist — preventing a partial state where some posts exist but their cursor keys don't.
Block 2: Room Schema
Interviewer: Walk me through the key entities.
Candidate:
plaintext
posts─────────────────────────────────────────────────id TEXT (PK) "t3_abc123"subreddit TEXTtitle TEXTauthorName TEXTscore INT denormalised — updated on votecommentCount INTpostType ENUM TEXT / IMAGE / VIDEO / LINKthumbnailUrl TEXT?imageUrl TEXT?videoUrl TEXT?selfText TEXT?userVoteDirection INT -1 / 0 / +1 — local vote statecachedAt LONG drives TTL invalidation strategycomments─────────────────────────────────────────────────id TEXT (PK)postId TEXT (FK)parentId TEXT post ID or parent comment IDbody TEXTscore INTdepth INT 0=top-level, 1=reply, 2=reply-reply…userVoteDirection INTisCollapsed BOOLEAN local-only — never synced to serverisHidden BOOLEAN local-only — child of a collapsed commentcachedAt LONGsubreddits─────────────────────────────────────────────────name TEXT (PK) "r/androiddev"displayName TEXTsubscriberCount INTisSubscribed BOOLEAN optimistic local statecachedAt LONG
Key design decisions:
userVoteDirection on both posts and comments powers the optimistic vote UI — the client owns this state and syncs asynchronously.
isCollapsed and isHidden on comments are local-only fields — they never sync to the server. isCollapsed tracks whether a user folded a branch; isHidden marks all children of a collapsed parent so the DAO query can filter them out. Persisting these in Room means the collapsed state survives screen navigation — Back → Forward → still collapsed.
cachedAt on every entity enables TTL-based cache invalidation without a separate tracking table.
Block 3: Optimistic Voting — The Most Common Follow-Up
Interviewer: The user taps upvote. Walk me through what happens — including the failure path.
Candidate: Three phases: immediate local update, background sync, and rollback on failure.
plaintext
TAP UPVOTE───────────────────────────────────────────────────────────ViewModel receives VoteEvent(postId, newDirection, prevDirection) │ ├─► Phase 1: Immediate Room write (< 50ms) │ UPDATE posts │ SET userVoteDirection = newDirection, │ score = score + delta │ WHERE id = postId │ │ Room emits → UI reacts → button colour changes, score updates │ └─► Phase 2: Enqueue VoteSyncWorker (WorkManager) UniqueWork name: "vote_{postId}" ExistingWorkPolicy: REPLACE Constraint: NetworkType.CONNECTED Backoff: EXPONENTIAL (10s, 20s, 40s…)VoteSyncWorker runs when network is available─────────────────────────────────────────────────────────── │ ├─ SUCCESS │ POST /vote { id, direction } → 200 OK │ Room state already correct — nothing to update │ └─ PERMANENT FAILURE (post deleted, auth error, etc.) Phase 3: Rollback UPDATE posts SET userVoteDirection = prevDirection, score = score - delta WHERE id = postId Room emits → vote indicator reverts Snackbar: "Vote couldn't be registered"
Interviewer: Why ExistingWorkPolicy.REPLACE?
Candidate: If the user taps upvote, then immediately taps again to remove it, two WorkManager tasks could be queued for the same post. REPLACE cancels the first and keeps only the latest. Without this, both tasks fire — the server sees upvote then removal in sequence, but on a slow network they could arrive in the wrong order. REPLACE ensures exactly one vote sync per post per moment.
Interviewer: What if the user votes while offline and then doesn't open the app for two days?
Candidate: WorkManager persists its queue to disk. The VoteSyncWorker is still queued with a CONNECTED constraint. When connectivity returns — even days later — it fires. The local Room state has shown the voted state all along, and it gets confirmed server-side when the worker runs. No user-visible disruption.
If you want to practice explaining these three phases clearly — and handling the follow-ups that come after — Mockingly.ai has Android social media simulations where optimistic UI with rollback is a standard deep-dive.
Block 4: Image Loading and Memory Management
Interviewer: How do you handle images in the feed without running into memory issues?
Candidate: Use Coil (Kotlin-first, coroutines-native) or Glide — both manage a two-layer cache: in-memory LRU and disk LRU. The two things that matter most for a feed:
1. Decode at display size, not source size.
If a thumbnail slot is 300×200dp, request the image at those exact dimensions. Coil's size(ViewSizeResolver(imageView)) does this automatically. Without it, a 4K JPEG decodes to ~32 MB in RAM. At display size, the same image is under 1 MB.
2. Cancel in-flight requests on ViewHolder recycle.
Both Coil and Glide cancel requests tied to an ImageView automatically when the view is recycled — no manual cleanup needed. The key is to always bind the load to the view's lifecycle, not to a coroutine scope that outlives the ViewHolder.
For videos: use a single shared ExoPlayer instance managed by a VideoPlaybackManager singleton — not one player per feed item. Multiple simultaneous ExoPlayer instances exhaust hardware codec slots. The manager tracks which video item is most visible (via scroll position) and routes the single player to that ViewHolder. When a new item becomes primary, the player is detached from the old one and attached to the new.
Multiple view types in the adapter: the adapter uses getItemViewType() to route each post to the correct ViewHolder — TextPostViewHolder, ImagePostViewHolder, VideoPostViewHolder, LinkPostViewHolder. This keeps onBindViewHolder clean — each holder only knows its own type, with no branching.
Block 5: Nested Comments
Interviewer: How do you render a deep comment tree without performance issues?
Candidate: Flatten the tree to a list using the depth field, then use isCollapsed/isHidden flags for collapse/expand — both stored locally in Room.
Flat list rendering:
plaintext
Comment tree (in Room): A (depth 0) B (depth 1) C (depth 2) D (depth 1)DAO query: SELECT * FROM comments WHERE postId = X AND isHidden = 0Result (flat list): A → rendered with no indent B → rendered with one-level indent C → rendered with two-level indent D → rendered with one-level indent
Collapse flow:
plaintext
User taps comment B header │ ▼ViewModel: UPDATE comments SET isCollapsed = true WHERE id = B UPDATE comments SET isHidden = true WHERE parentId = B (recursive) │ ▼DAO query re-runs: isHidden = 0 filters C out of results │ ▼DiffUtil computes minimal diff — C disappears from list in-placeComment B now shows "▶ 1 reply collapsed"
Two-pass loading for large threads: on post open, load only top-level comments and 2 levels deep — enough for the user to start reading immediately. Deeper sub-trees are loaded lazily when the user taps a "Load N more replies" placeholder. This is how Reddit's API works natively, returning moreChildren tokens for truncated branches.
Block 6: Offline Strategy
Interviewer: How does the app behave with no internet?
Candidate: Room-first, always. The key decisions:
Cache freshness via InitializeAction:
kotlin
override suspend fun initialize(): InitializeAction { val age = System.currentTimeMillis() - (latestCachedPost?.cachedAt ?: 0) return if (age < TimeUnit.HOURS.toMillis(1)) InitializeAction.SKIP_INITIAL_REFRESH // serve from cache, skip network else InitializeAction.LAUNCH_INITIAL_REFRESH // fetch in background}
When cache is fresh, no network call happens on app open — the feed renders instantly from Room. When cache is stale, RemoteMediator fetches in the background while the stale data is already showing.
Pending writes while offline: votes and other write actions are queued via WorkManager with a CONNECTED constraint. They execute automatically when network returns — even if the app was killed in between. Room shows the optimistic state throughout. No user action required to replay pending writes.
Block 7: Push Notifications and Deep Linking
Interviewer: The user gets a notification — "Alice replied to your comment." They tap it. What happens?
Candidate: Two systems: FCM for delivery, Jetpack Navigation for landing.
FCM data message (not notification message):
plaintext
Server sends FCM data payload: { type: "comment_reply", post_id: "t3_abc123", comment_id: "t1_xyz789", author: "Alice", preview: "That's a great point..." }FirebaseMessagingService.onMessageReceived() → Parse payload → Optionally write to Room (pre-load the post/comment data) → Post MessagingStyle notification with a deep link PendingIntent
Data messages (not notification messages) give full control — onMessageReceived() fires in all app states, the app decides what to display, and the notification can be suppressed if the user is already on the target screen.
Deep linking via NavDeepLinkBuilder:
kotlin
val pendingIntent = NavDeepLinkBuilder(context) .setGraph(R.navigation.nav_graph) .setDestination(R.id.postDetailFragment) .setArguments(bundleOf("postId" to postId, "highlightCommentId" to commentId)) .createPendingIntent()
NavDeepLinkBuilder uses TaskStackBuilder under the hood to synthesise the correct back stack. When the user presses Back from the deep-linked screen, they land on the home feed — not exit the app. Without this, Back would exit the app, which is a common bug in notification implementations.
In PostDetailFragment: if highlightCommentId is present in the args, scroll the comment list to that comment and briefly highlight it. Load from Room first (likely already cached), fall back to network if not.
Block 8: Subreddit Subscription
Interviewer: The user subscribes to a subreddit. How does that flow work?
Candidate: Same optimistic pattern as voting.
plaintext
User taps Subscribe │ ▼Room: UPDATE subreddits SET isSubscribed = true WHERE name = XUI updates immediately — button state changesSubscribeWorker enqueued (WorkManager, CONNECTED constraint) │ ├─ SUCCESS → Room state correct — nothing to do └─ FAILURE → Rollback: SET isSubscribed = false Snackbar: "Couldn't subscribe. Try again."
The home feed DAO query joins posts against subreddits WHERE isSubscribed = true. When subscription state changes in Room, the feed updates reactively — no manual refresh needed.
Common Interview Follow-ups
"How do you prevent the feed jumping when new posts load?"
On pull-to-refresh, adapter.refresh() triggers a REFRESH load. Room clears and replaces from the top. The list resets to position 0 with new content — intentional and expected on explicit refresh. For a "N new posts" banner (Twitter-style), buffer new posts and only insert them when the user taps the banner, not automatically mid-session.
"How do you handle a post that's deleted by the server while the user is offline, but they voted on it locally?"
VoteSyncWorker gets a 404. The worker catches this as non-retryable, calls Result.failure(), and rolls back the local vote in Room. The post will also be missing from the next delta sync. The Room DAO query for the feed naturally excludes it after the next refresh. No crash, no infinite retry, graceful degradation.
"How do you handle 1,000 comments without a long initial wait?"
Two-pass loading. Pass 1: fetch top-level comments and 2 levels deep on post open — renders immediately. Pass 2: deeper sub-trees load lazily on "Load N more" tap. Reddit's own API returns moreChildren tokens for truncated branches. The client only fetches what the user actually expands.
"How does the search work?"
Subreddit search uses debounced network calls (300ms after last keystroke) with results cached in Room for 5 minutes. Post search uses a network-only PagingSource — no RemoteMediator, no Room cache. Search results are too query-specific to cache meaningfully. The simpler PagingSource is the right trade-off here.
Quick Interview Checklist
✅ Clarified scope — feed, subreddit, post detail, votes, media, notifications, deep linking
✅ Clean Architecture + MVVM/MVI — single UiState for complex multi-dimensional state
✅ Room as single source of truth — network fills Room, UI observes Room exclusively
✅ Paging 3 with RemoteMediator — cursor-based ("after" token), not offset pagination
✅ RemoteKeys table — stores cursor alongside posts for stable pagination across sessions
✅ SKIP_INITIAL_REFRESH for fresh cache — no network wait on fast opens
✅ withTransaction wrapping Room writes — atomicity on page load
✅ REFRESH clears and replaces; APPEND only inserts new rows
Designing Reddit for Android is a state management problem with a content feed on top. Every user action — vote, collapse, subscribe — is a local Room mutation first and a network sync second. The offline resilience and rollback paths for each action are as important as the happy path.
In real interviews, sketching the high-level architecture, explaining pagination and optimistic UI updates, describing the offline strategy for queuing votes and syncing them later, and discussing consistency trade-offs are the core expectations.
The design pillars:
Room as single source of truth — the network fills Room; the UI observes Room only
Paging 3 with RemoteMediator — cursor-based, transactional, InitializeAction for fast opens
Optimistic mutations with WorkManager rollback — votes, subscriptions, posts — all follow the same three-phase pattern
ExistingWorkPolicy.REPLACE — ensures latest user intent wins; no stale syncs
Decode images at display size — the single most impactful memory safety measure
Flat list for comments — depth for nesting, Room flags for collapse state, two-pass loading for large threads
NavDeepLinkBuilder for notifications — correct back stack, no "exit app on Back" bug
Frequently Asked Questions
What is optimistic UI and how does it work for voting in Android?
Optimistic UI is a pattern where the interface updates immediately to reflect a user action — before the server has confirmed it — then rolls back if the server rejects it.
How it works for an upvote in three phases:
Immediate Room write — UPDATE posts SET userVoteDirection = 1, score = score + 1 WHERE id = postId. Room emits the change, the vote button changes colour and the score increments in under 50ms. No network call has happened yet
Background WorkManager sync — VoteSyncWorker is enqueued with ExistingWorkPolicy.REPLACE and a CONNECTED constraint. It runs when network is available and POSTs the vote to the server
Rollback on permanent failure — if the server returns a non-retryable error (post deleted, auth expired), the worker reverts the Room update to the previous userVoteDirection and score. Room emits again, the UI reverts, and a Snackbar notifies the user
The result: the user gets instant feedback, the server eventually receives the vote, and failures are handled gracefully without silent data corruption.
What is the difference between PagingSource and RemoteMediator in Paging 3?
PagingSource loads data from a single source. RemoteMediator coordinates between a remote source (network) and a local cache (Room), using Room as the single source of truth.
PagingSource
RemoteMediator
Data source
One source only
Network + Room together
Offline support
No — fails if source unavailable
Yes — serves cached Room data instantly
Complexity
Simple
More setup required
Best for
Network-only lists (e.g. search results)
Feeds that need caching and offline access
How RemoteMediator works for a Reddit feed:
The Pager always reads from Room's PagingSource — the UI observes only Room
When the user scrolls near the end, Paging 3 calls RemoteMediator.load(APPEND)
The RemoteMediator fetches the next page from the network using a cursor token
New posts are written to Room in a withTransaction block
Room's PagingSource detects the new rows and emits the updated list automatically
Use RemoteMediator for any feed that needs to work offline and show cached content instantly on app open. Use a plain PagingSource for search results or any list that is always fresh from the network.
Why use cursor-based pagination instead of offset pagination for the Reddit feed?
Cursor-based pagination anchors to a specific item ID. Offset pagination uses a page number or row offset. For a live feed, offset breaks — cursor does not.
The problem with offset on a live feed:
The feed at /posts?page=2 returns rows 26–50 when page 1 loaded
While the user reads page 1, 5 new posts are added at the top
When page 2 loads, rows 26–50 now include 5 posts the user already saw on page 1
The user sees duplicates — or worse, misses posts entirely if items were removed
How cursor-based fixes this:
Page 1 returns posts 1–25 and an after cursor = "t3_xyz" (the ID of post 25)
Page 2 fetches /posts?after=t3_xyz — returns the 25 posts after that specific item
New posts added at the top do not affect the cursor anchor
No duplicates, no skipped posts, regardless of feed activity
Reddit's own API uses the after token for exactly this reason. The RemoteKeys Room table stores this cursor token alongside each post so Paging 3 can resume pagination correctly after process kills.
How do you implement optimistic voting with rollback in Android?
Optimistic voting with rollback means updating Room immediately on tap, syncing to the server in the background, and reverting the Room state if the sync permanently fails.
Full implementation flow:
User taps upvote → ViewModel receives VoteEvent(postId, newDirection=1, prevDirection=0)
ExistingWorkPolicy.REPLACE — cancels any pending vote sync for the same post
NetworkType.CONNECTED constraint — waits for connectivity
Exponential backoff — retries on transient failures (network error, 429)
On server SUCCESS — Room already reflects the correct state, nothing to update
On IOException or 429 — Result.retry(), backoff applies
On permanent failure (404 post deleted, 401 auth) — rollback Room to prevDirection and revert score
Why ExistingWorkPolicy.REPLACE is critical:
If the user taps upvote then immediately taps again to remove the vote, two workers would be queued for the same post. REPLACE cancels the first and keeps only the latest — ensuring the final server state matches the user's last intent.
How do you flatten and render a nested comment tree in a RecyclerView?
Nested comments are flattened to a list using a depth field on each comment, with isCollapsed and isHidden flags driving collapse and expand — all stored in Room.
The data model:
Each CommentEntity has a depth INT (0 = top-level, 1 = reply, 2 = reply-to-reply…)
isCollapsed — local-only flag, never synced to server. True when the user taps the comment header to fold the branch
isHidden — local-only flag. Set to true on all children of a collapsed comment
The Room DAO query filters: SELECT * FROM comments WHERE postId = X AND isHidden = 0
Collapse flow:
User taps comment header → isCollapsed = true on that comment, isHidden = true on all children
Room emits the filtered list — hidden comments disappear from the result
ListAdapter DiffUtil computes the minimal diff — only the collapsed rows are removed
The collapsed comment shows a "▶ N replies" indicator
Tap again → isCollapsed = false, isHidden = false on direct children → they reappear in-place
Why store these in Room (not just ViewModel memory)?
Collapse state survives screen navigation. If the user collapses a branch, taps Back, and returns — the branch is still collapsed. In-memory state would reset to fully expanded on every screen enter.
How does the offline feed work with Room and RemoteMediator?
Offline feed support means the user sees the last cached posts from Room immediately — with no network required and no blank screen.
How it works:
On app open, Paging 3 reads from Room's PagingSource instantly — cached posts render before any network call
RemoteMediator.initialize() checks the cachedAt timestamp of the most recent post:
If cache is fresh (< 1 hour old) → returns SKIP_INITIAL_REFRESH. No network call, cached data shows instantly
If cache is stale → returns LAUNCH_INITIAL_REFRESH. Fetches fresh data in background while stale cache is already showing
If the device is offline, RemoteMediator.load() throws IOException → returns MediatorResult.Error. Paging 3 surfaces a retry footer, but the cached data already on screen remains visible
ConnectivityManager.NetworkCallback.onAvailable() fires when connectivity returns → RemoteMediator automatically retries REFRESH → fresh posts write to Room → UI updates reactively
Pending writes while offline:
Votes and subscriptions made offline are queued by WorkManager with a CONNECTED constraint. When network returns, WorkManager fires the queued jobs automatically — even if the app was killed between going offline and reconnecting.
When should you use MVI instead of MVVM for an Android feed?
MVI (Model-View-Intent) uses a single immutable UiState object observed by the UI. MVVM typically exposes multiple independent StateFlow or LiveData fields.
MVI is the better choice when UI state has multiple dimensions that change independently:
Situation
MVVM risk
MVI solution
Feed loading + per-item vote state
Two StateFlows can diverge — UI shows stale vote on refresh
Single UiState updated atomically
Error state + partial data
Error clears the list, losing scroll position
UiState(error=..., posts=cachedList) preserves both
Offline banner + loading indicator
Two separate flags can both be true simultaneously
State machine enforces valid state combinations
Feed refresh + append in flight
Refresh cancels append but flags don't clear
Single isRefreshing vs isAppending field in UiState
For a Reddit-style feed specifically:
The feed has loading, error, empty, refreshing, offline, and per-item vote states — all potentially active at different times. A single UiState data class prevents the state inconsistencies that come from observing six separate StateFlow fields. For simpler screens with one or two states, MVVM is fine.
How do you implement deep linking from a push notification with the correct back stack?
NavDeepLinkBuilder constructs a PendingIntent that synthesises the correct back stack so the user lands on the right screen and Back navigates to the home feed — not exits the app.
How to implement it correctly:
Server sends an FCM data message (not a notification message) with post_id and comment_id
FirebaseMessagingService.onMessageReceived() parses the payload
Build the PendingIntent:
kotlin
val pendingIntent = NavDeepLinkBuilder(context) .setGraph(R.navigation.nav_graph) .setDestination(R.id.postDetailFragment) .setArguments(bundleOf( "postId" to postId, "highlightCommentId" to commentId )) .createPendingIntent()
Attach to a NotificationCompat.Builder as the contentIntent
When tapped, the user lands on PostDetailFragment with the post pre-loaded
The synthesised back stack means Back → HomeFragment, not app exit
Why FCM data messages, not notification messages?
Notification messages are handled by the OS — onMessageReceived() is never called when the app is in the background. The app cannot suppress the notification if the user is already on that screen, cannot pre-load Room data, and cannot customise the PendingIntent. Data messages give the app full control in all states.
Which companies ask the Android Reddit system design question?
Reddit, Meta, Google, TikTok, Pinterest, and Twitter (X) ask variants of this question for senior Android engineer roles.
Why it is a popular interview question:
State management complexity — voting, collapsing, subscribing all require optimistic updates with rollback
Feed breadth — covers Paging 3, Room, image loading, offline strategy, and media handling in a single question
Scales to seniority — a junior candidate talks about showing a list; a senior candidate explains cursor pagination, ExistingWorkPolicy.REPLACE, isHidden comment flags, and InitializeAction.SKIP_INITIAL_REFRESH
Real product — every company listed either runs a feed product or a social platform with voting, making the answer directly applicable
What interviewers specifically listen for:
Explaining why cursor-based, not offset pagination — and the duplicate-post failure mode
ExistingWorkPolicy.REPLACE — and the specific race condition it prevents
isCollapsed / isHidden stored in Room — not in ViewModel memory — and why
SKIP_INITIAL_REFRESH for fast app opens — signals deep Paging 3 knowledge
Reddit interviews often go off-script fast — vote rollback under no signal, comment collapse persistence across navigation, what happens when a post is deleted mid-sync. Being fluent in these edge cases under real interview pressure is a different skill from understanding them on paper. Mockingly.ai has Android-focused system design simulations for engineers preparing for senior roles at Reddit, Meta, Google, and TikTok.