Design Android MVVM API Architecture
Company: OpenAI
Role: Android Engineer
Category: System Design
Difficulty: medium
Interview Round: Technical Screen
Design the **client-side architecture** for a single Android screen that loads data from a remote HTTP API and renders it in a Jetpack Compose UI, using an **MVVM-style** architecture.
Walk through the full request-to-render path and be ready to defend each decision — the interviewer cares as much about *why* you make a choice as *what* you choose. Lead with a layered MVVM decomposition, then drill into the rest. Your design must address:
- **HTTP request construction and execution** — how the request is built, sent, parsed, and how transport errors are surfaced to upper layers.
- **Threading, concurrency, cancellation, and race conditions** — what scope work runs in, how stale work is cancelled, and how out-of-order responses are prevented from corrupting state.
- **UI states** — loading, success, empty, and error, including retry behavior.
- **Navigation** between screens, and where navigation logic should live.
- **ViewModel lifecycle management** across three setups: (a) Java-based Android, (b) Kotlin with coroutines/`Flow`, and (c) Kotlin with Jetpack Compose. For each, explain the lifecycle, how state is exposed, and the pitfalls to watch for.
- **Alternative designs, trade-offs, and possible optimizations.**
```hint Where to start — the layering
Settle the layering before anything else: how many layers are there, and which single layer is allowed to know HTTP exists? Ask yourself whether the Compose UI should ever be able to reach the network client — if not, what does that imply about the direction of dependencies between UI, presentation, and data? Also pin down the one invariant about *which way data moves vs. which way user actions move*. Getting that contract right organizes every later decision.
```
```hint Concurrency & race conditions
First decide which scope the fetch runs in so that leaving the screen cleans up in-flight work for free (and why a process-wide scope is the wrong answer). Then sit with the nasty case: the user taps retry several times and a *slower, older* request finishes last. What guarantees only the newest result reaches the UI? There's a declarative answer built from flow operators and a manual-bookkeeping answer — try to reach both and reason about which you'd default to.
```
```hint Modeling UI state
The ViewModel should expose **one immutable source of truth** — but its *shape* is a real decision. Compare a closed set of mutually-exclusive states against a single bag of fields, and ask which one lets you keep showing old data while a refresh runs (think pull-to-refresh) and which one makes illegal combinations impossible. Then ask a modeling question: is "the request succeeded but returned nothing" the *same* outcome as "the request failed"? Compose should render as a pure function of whatever you pick.
```
```hint The three lifecycle setups
Resist describing three different *lifecycles* — first convince yourself whether the `ViewModel` lifecycle even changes across Java, Kotlin+Flow, and Compose. If it doesn't, then what actually differs between the three? Frame it as: *how is async work observed, and how does state reach the view* in each setup. For each of the three, name the single pitfall most likely to bite (e.g. what does the VM hold that leaks, what gets collected wrong, where does a side effect get misplaced).
```
```hint Navigation & one-time events
Decide where the navigation API lives — should the ViewModel know about the navigator at all, or should the screen receive actions to perform? Then handle the subtler case: navigation that must happen *because* async logic succeeded. Ask what category of thing that is — is it part of the render state, or something else? — and what goes wrong if you model it as state and a config change or recomposition replays it. That failure mode points you at the fix.
```
### Constraints & Assumptions
- One screen that fetches a list (or detail) over HTTPS from a backend you do not control. Assume a typical JSON REST endpoint.
- Compose is the rendering layer; assume modern AndroidX (`ViewModel`, `viewModelScope`, `StateFlow`, Navigation-Compose available) and a modern HTTP stack (Retrofit + OkHttp, or Ktor).
- The main thread must never block; the UI must stay responsive during the network call.
- Production concerns matter: configuration changes (rotation), process death/restoration, flaky networks, auth via bearer token, leak-freedom, and testability.
- You are not asked to write a full app — focus on the layering, the data/state flow, and the lifecycle reasoning. Pseudocode and interface sketches are fine.
### Clarifying Questions to Ask
- Is this a list screen or a detail screen, and does it paginate? Does it need pull-to-refresh?
- Is the data cached/persisted (offline support, single-source-of-truth from a DB), or is it network-only?
- How is auth handled — bearer token, refresh-on-401, anything the client must inject per request?
- What is the existing codebase: Java/XML, Kotlin/Views, or Compose? Any team conventions or DI framework in place?
- Are there strict latency/error-rate targets, or analytics/observability requirements?
- Should multiple sections of the screen load independently (partial loading), or is it one atomic fetch?
### What a Strong Answer Covers
- A clean layered MVVM split (UI / ViewModel / Repository / data source) with a clear rationale for each boundary and what crosses it.
- HTTP details (method, headers/auth, serialization, timeouts) confined to the data layer; the ViewModel sees only a domain result type, not raw HTTP.
- Structured concurrency: correct scope choice, automatic cancellation, and an explicit strategy for stale/out-of-order responses.
- An exhaustive, single-source-of-truth UI state model with loading/success/empty/error and retry, and a justification of sealed-class vs data-class.
- Correct, *differentiated* lifecycle reasoning for Java, Kotlin+Flow, and Compose — including the common pitfalls of each (Activity refs in the VM, duplicate jobs, lifecycle-unaware collection, side effects in the composable body).
- Navigation kept out of the ViewModel, with a deliberate one-time-event story.
- Error taxonomy + retry policy (manual vs auto-backoff, idempotency awareness) and concrete optimizations (caching, paging, recomposition stability, request dedup, instrumentation).
- Alternatives (MVC/MVP/MVI/Clean Architecture, LiveData vs StateFlow) compared on trade-offs, with no claim that one is universally best.
- A testing strategy spanning the layers.
### Follow-up Questions
- Process death restores the Activity but your `viewModelScope` job was killed mid-flight. How do you restore state and avoid showing a permanent spinner? Where does `SavedStateHandle` fit?
- The screen makes three independent API calls and one fails. Walk through how you'd represent and render a partial-failure state.
- A user double-taps a non-idempotent "submit" that triggers a POST. How does your design prevent a duplicate write, both client-side and end-to-end?
- Compare `LiveData`, `StateFlow`, and `SharedFlow` for exposing UI state vs one-time events. When does each leak or misbehave, and which would you default to?
Quick Answer: This question evaluates a candidate's skill in designing Android client-side MVVM architecture, testing competencies in layered decomposition, HTTP request lifecycle, threading and cancellation, ViewModel lifecycle across Java/Kotlin/Compose, UI state modeling, and navigation; the domain is system design for Android application architecture.