Pinterest Interview Question

Design Reddit (Android) — Pinterest Interview

hard20 minAndroid System Design

How Pinterest Tests This

Pinterest interviews focus on content feed systems, image processing, recommendation engines, distributed task scheduling, and social platform architecture. They test your ability to design visual discovery systems at scale.

Interview focus: Social feeds, task scheduling, content recommendation, and Android system design.

Key Topics
redditpaging3roomworkmanageroffline firstoptimistic uikotlin

Android System Design: Design Reddit

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
  • Post creation: text and image posts
  • Push notifications: replies, mentions, comment responses
  • 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 page
Post metadata:         ~2 KB per post
Thumbnail:             ~150–300 KB (CDN-served)
Comment depth:         Reddit caps at 8 levels
Comments 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.

plaintext
┌──────────────────────────────────────────────────┐
│                   UI Layer                        │
│  HomeScreen  SubredditScreen  PostDetailScreen    │
│         ↑ observes StateFlow / PagingData         │
│              ViewModel (MVVM / MVI)               │
└───────────────────┬──────────────────────────────┘

┌───────────────────▼──────────────────────────────┐
│                Domain Layer                       │
│  Use Cases: GetFeed, Vote, GetComments,           │
│             Subscribe, GetPost, Search            │
└───────────────────┬──────────────────────────────┘

┌───────────────────▼──────────────────────────────┐
│                 Data Layer                        │
│                                                   │
│  FeedRepository                                   │
│    ├── RemoteDataSource  (Retrofit)               │
│    └── LocalDataSource   (Room)                   │
│                                                   │
│  VoteRepository                                   │
│    ├── Room  (instant optimistic write)           │
│    └── WorkManager  (VoteSyncWorker)              │
└──────────────────────────────────────────────────┘

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 updates
 
SCROLL 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       TEXT
title           TEXT
authorName      TEXT
score           INT            denormalised — updated on vote
commentCount    INT
postType        ENUM           TEXT / IMAGE / VIDEO / LINK
thumbnailUrl    TEXT?
imageUrl        TEXT?
videoUrl        TEXT?
selfText        TEXT?
userVoteDirection INT          -1 / 0 / +1 — local vote state
cachedAt        LONG           drives TTL invalidation strategy
 
comments
─────────────────────────────────────────────────
id              TEXT (PK)
postId          TEXT (FK)
parentId        TEXT           post ID or parent comment ID
body            TEXT
score           INT
depth           INT            0=top-level, 1=reply, 2=reply-reply…
userVoteDirection INT
isCollapsed     BOOLEAN        local-only — never synced to server
isHidden        BOOLEAN        local-only — child of a collapsed comment
cachedAt        LONG
 
subreddits
─────────────────────────────────────────────────
name            TEXT (PK)      "r/androiddev"
displayName     TEXT
subscriberCount INT
isSubscribed    BOOLEAN        optimistic local state
cachedAt        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.

plaintext
Image request lifecycle:
  onBind()      → Coil.load(url) → checks memory cache → checks disk cache → network
  onViewRecycled() → Coil cancels in-flight request automatically

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 = 0
Result (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-place
Comment 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.

Offline UX:

plaintext
App opens offline


Room has cached posts → renders immediately
RemoteMediator.load() → IOException → MediatorResult.Error


Paging 3 shows cached data + offline footer state
ConnectivityManager detects offline → "Offline" banner (non-blocking)
 
Network returns


NetworkCallback.onAvailable() fires
  → Banner dismisses
  → RemoteMediator retries REFRESH automatically
  → Fresh posts write to Room → UI updates

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 = X
UI updates immediately — button state changes
 
SubscribeWorker 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
  • ✅ Room schema: userVoteDirection, isCollapsed/isHidden (local-only), cachedAt
  • ✅ Optimistic voting: Room write → WorkManager sync → rollback on permanent failure
  • ExistingWorkPolicy.REPLACE — latest vote wins, stale syncs cancelled
  • ✅ WorkManager queued with CONNECTED constraint — replays on network return, survives process kill
  • ✅ Image loading at display size — prevents OOM; in-flight cancel on ViewHolder recycle
  • ✅ Single shared ExoPlayer via VideoPlaybackManager — no per-item player
  • ✅ Multiple ViewHolder types — no branching in onBindViewHolder
  • ✅ Comment tree flattened via depthisCollapsed/isHidden drive in-place collapse
  • ✅ Two-pass comment loading — top levels upfront, deep branches on tap
  • ✅ Offline: InitializeAction for fast opens, WorkManager queues pending writes
  • ✅ FCM data messages — onMessageReceived() in all states, suppress if already on target screen
  • NavDeepLinkBuilder — synthesises correct back stack from notification tap
  • ✅ Subreddit subscription: optimistic Room write → WorkManager sync → rollback

Conclusion

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:

  1. Room as single source of truth — the network fills Room; the UI observes Room only
  2. Paging 3 with RemoteMediator — cursor-based, transactional, InitializeAction for fast opens
  3. Optimistic mutations with WorkManager rollback — votes, subscriptions, posts — all follow the same three-phase pattern
  4. ExistingWorkPolicy.REPLACE — ensures latest user intent wins; no stale syncs
  5. Decode images at display size — the single most impactful memory safety measure
  6. Flat list for commentsdepth for nesting, Room flags for collapse state, two-pass loading for large threads
  7. 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:

  1. Immediate Room writeUPDATE 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
  2. Background WorkManager syncVoteSyncWorker is enqueued with ExistingWorkPolicy.REPLACE and a CONNECTED constraint. It runs when network is available and POSTs the vote to the server
  3. 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.

PagingSourceRemoteMediator
Data sourceOne source onlyNetwork + Room together
Offline supportNo — fails if source unavailableYes — serves cached Room data instantly
ComplexitySimpleMore setup required
Best forNetwork-only lists (e.g. search results)Feeds that need caching and offline access

How RemoteMediator works for a Reddit feed:

  1. The Pager always reads from Room's PagingSource — the UI observes only Room
  2. When the user scrolls near the end, Paging 3 calls RemoteMediator.load(APPEND)
  3. The RemoteMediator fetches the next page from the network using a cursor token
  4. New posts are written to Room in a withTransaction block
  5. 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:

  1. The feed at /posts?page=2 returns rows 26–50 when page 1 loaded
  2. While the user reads page 1, 5 new posts are added at the top
  3. When page 2 loads, rows 26–50 now include 5 posts the user already saw on page 1
  4. The user sees duplicates — or worse, misses posts entirely if items were removed

How cursor-based fixes this:

  1. Page 1 returns posts 1–25 and an after cursor = "t3_xyz" (the ID of post 25)
  2. Page 2 fetches /posts?after=t3_xyz — returns the 25 posts after that specific item
  3. New posts added at the top do not affect the cursor anchor
  4. 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:

  1. User taps upvote → ViewModel receives VoteEvent(postId, newDirection=1, prevDirection=0)
  2. ViewModel writes to Room: userVoteDirection = 1, score = score + 1
  3. UI reacts to Room Flow emission — button colour changes, score updates (under 50ms)
  4. WorkManager enqueues VoteSyncWorker with:
    • 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)
  5. On server SUCCESS — Room already reflects the correct state, nothing to update
  6. On IOException or 429Result.retry(), backoff applies
  7. 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:

  1. Each CommentEntity has a depth INT (0 = top-level, 1 = reply, 2 = reply-to-reply…)
  2. isCollapsed — local-only flag, never synced to server. True when the user taps the comment header to fold the branch
  3. isHidden — local-only flag. Set to true on all children of a collapsed comment
  4. The Room DAO query filters: SELECT * FROM comments WHERE postId = X AND isHidden = 0

Collapse flow:

  1. User taps comment header → isCollapsed = true on that comment, isHidden = true on all children
  2. Room emits the filtered list — hidden comments disappear from the result
  3. ListAdapter DiffUtil computes the minimal diff — only the collapsed rows are removed
  4. The collapsed comment shows a "▶ N replies" indicator
  5. 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:

  1. On app open, Paging 3 reads from Room's PagingSource instantly — cached posts render before any network call
  2. 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
  3. 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
  4. 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:

SituationMVVM riskMVI solution
Feed loading + per-item vote stateTwo StateFlows can diverge — UI shows stale vote on refreshSingle UiState updated atomically
Error state + partial dataError clears the list, losing scroll positionUiState(error=..., posts=cachedList) preserves both
Offline banner + loading indicatorTwo separate flags can both be true simultaneouslyState machine enforces valid state combinations
Feed refresh + append in flightRefresh cancels append but flags don't clearSingle 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:

  1. Server sends an FCM data message (not a notification message) with post_id and comment_id
  2. FirebaseMessagingService.onMessageReceived() parses the payload
  3. 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()
  1. Attach to a NotificationCompat.Builder as the contentIntent
  2. When tapped, the user lands on PostDetailFragment with the post pre-loaded
  3. 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:

  1. State management complexity — voting, collapsing, subscribing all require optimistic updates with rollback
  2. Feed breadth — covers Paging 3, Room, image loading, offline strategy, and media handling in a single question
  3. 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
  4. 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:

  1. Explaining why cursor-based, not offset pagination — and the duplicate-post failure mode
  2. The three-phase optimistic vote pattern — Room write, WorkManager sync, rollback
  3. ExistingWorkPolicy.REPLACE — and the specific race condition it prevents
  4. isCollapsed / isHidden stored in Room — not in ViewModel memory — and why
  5. 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.

Companies That Ask This

Related System Design Guides

Design a Photo Sharing App (Android)

A complete Android system design guide for designing a photo sharing app like Instagram. Covers Clean Architecture, image loading with Glide vs Coil, multi-layer caching, RecyclerView performance, photo upload pipeline with pre-signed URLs and WorkManager, feed pagination with Paging 3, offline support, media permissions across Android versions, and memory management — all in a real interview conversation format for senior Android engineers.

hard20 min

Design a Notes App (Android)

A complete Android system design guide for designing a notes app like Google Keep or Apple Notes. Covers offline-first architecture with Room, sync conflict resolution strategies, delta sync, rich text storage with Spanned vs Markdown, full-text search with Room FTS4, WorkManager background sync, attachment handling, and multi-device consistency — all in a real interview conversation format for senior Android engineers.

medium20 min

Design a Trading App like Coinbase / Bitvavo (Android)

A complete Android system design guide for designing a crypto trading app like Coinbase or Bitvavo. Covers Clean Architecture for financial apps, WebSocket channel management, real-time data pipeline with Kotlin Flow, order book rendering with throttled DiffUtil, candlestick chart architecture, Room schema for portfolio and trade history, offline resilience, price alert pipeline with WorkManager, and battery-aware subscriptions — all in a real interview conversation format for senior Android engineers.

hard22 min