Mobile architecture
MVVM, MVI, TCA, Redux or VIPER? The senior answer names the pressure first
"Which architecture would you choose?" sounds like a pattern quiz. It is scored on whether you name the pressure before the pattern, and whether you know what each pattern costs to leave. A worked answer from the hiring side, for iOS and Android.
10 min read
The question arrives in many forms. "Which architecture do you use?" "MVVM or MVI for this screen?" "Would you adopt TCA here?" "Why not VIPER?" It sounds like a quiz on acronyms, and the quick answers treat it like one: MVVM because it is testable, TCA because it is modern, Redux because state should live in one place.
I have run 500+ technical interviews from the hiring side over 15 years in mobile. A confident pattern name in the first sentence is rarely what separates levels. What separates them is whether the candidate can say which problem the pattern is solving here, and what it will cost. The pattern is the last word of a good answer, not the first.
Controlled Change, my book on mobile architecture, discusses MVVM, MVI, Redux, TCA, VIPER, RIBs and Workflow without crowning any of them. Each one, in its words, is examined "as a response to pressure: state pressure, ownership pressure, failure pressure, team pressure, delivery pressure, or change pressure." This answer uses those six pressures as its spine. For the follow-up that usually comes next, see where would you not use it.
- The prompt: choose an architecture for a feature, or defend the one you use.
- The shape: clarify, name the pressure, place state and effects, pick per feature, price the exit.
- The test underneath: do requirements drive the pattern, or does the pattern arrive first.
The ten minutes, in order
| Minutes | What you do | What the interviewer is checking |
|---|---|---|
| 0 to 2 | Ask about the feature, the team, the failure cases | Do you ask before you choose? |
| 2 to 4 | Name the dominant pressure | Can you say why a pattern is needed at all? |
| 4 to 7 | Place state, effects and their lifetimes | Who owns state, and what survives? |
| 7 to 9 | Pick per feature, with its cost | Can you argue against your own choice? |
| 9 to 10 | Say how a feature would leave it | Is the decision reversible? |
In a full system design round this is a short segment, not the whole hour. Spend it in this order and the pattern name lands as a conclusion the interviewer has already watched you reach.
Six pressures, six questions
Before any pattern, find out which pressure dominates. Each one comes with a question you can ask out loud:
| Pressure | The question to ask | What it pushes toward |
|---|---|---|
| State | How many states and transitions, how many event sources? | Local state for simple screens, explicit transitions or a state machine for consequential flows |
| Ownership | Who may change this state, and how many writers are there? | One owner per piece of state, others observe or send commands |
| Failure | What happens when the response is lost, or the process dies mid-flow? | Explicit uncertain states, durable work owned outside the view |
| Team | How many teams change this code, and how often do they collide? | Feature boundaries and isolation, the pressure VIPER and RIBs were built for |
| Delivery | How is it released, flagged and rolled back? | Patterns whose framework types stay out of public contracts |
| Change | How often will this change, and how would we undo the choice? | Low coupling, a planned exit, a pilot before a mandate |
Most features have one or two dominant pressures. A settings toggle has none worth a framework. A checkout has state, ownership and failure pressure at once. Saying which pressures matter here is the move that turns a quiz into a design discussion.
State first: most pattern debates are state debates
The book's chapter on state ownership opens with the line I would most like candidates to internalize: "Most mobile architecture arguments are state arguments in disguise." The decisive questions are who owns mutable state, how changes enter, how effects are represented, and what happens when what you observe and what is authoritative disagree.
So show the state before the pattern. Give each piece of mutable state one owner, and make impossible combinations unrepresentable instead of guarding against them with booleans:
sealed interface CheckoutState {
data class Editing(val draft: Draft) : CheckoutState
data class Submitting(val operationId: String, val draft: Draft) : CheckoutState
data class OutcomeUnknown(val operationId: String, val draft: Draft) : CheckoutState
data class Confirmed(val bookingId: String) : CheckoutState
data class RequiresAction(val reason: String) : CheckoutState
}OutcomeUnknown is the state that wins the point. A request can reach the server, commit, and lose its response on the way back. A model built from isLoading, error and booking fields has nowhere honest to put that case, so it usually ends up as an error and invites a second booking. Once the states are named, the pattern question becomes concrete: which mechanism owns these transitions with the least ceremony for this team.
Mobile architecture patterns compared: what each buys
Each pattern is a compressed trade-off. Name the trade, the way it fails, and the signal that it no longer pays:
| Pattern | Strong fit | How it fails | Signal it no longer pays |
|---|---|---|---|
| Direct state | A small screen with a draft, validation and one submission | Payment, restoration and unknown outcomes arrive | Transitions and effects become hard to trace |
| MVVM | Forms, lists, workflows with straightforward transitions | The ViewModel becomes a dumping ground; two-way bindings hide mutation | It absorbs navigation, networking and analytics: split ownership, do not add a layer |
| MVI / UDF | Consequential workflows, many event sources, complex partial states | Giant state and action types, every keystroke an event | Transitions are trivial and the action vocabulary is overhead |
| Redux | Shared application state, deterministic processing, replay and debugging | Global-state convenience, action inflation, hidden middleware order | Most slices are observed by one feature each |
| TCA | Swift teams adopting it deeply; rich state and effects; exhaustive tests | Framework coupling, learning curve, over-modeling trivial UI | Its types leak into public feature contracts, so no feature can leave |
| VIPER / RIBs | Organizational scale, strict roles, many teams in parallel | Files and protocols per screen, ceremony on every feature | The scale that justified it is not there |
The last row is the one candidates miss. VIPER and RIBs answer team pressure more than feature complexity. Without that organizational scale, the ceremony is paid on every screen and returns little. Saying that out loud shows you know why the pattern exists, not only what its letters stand for.
Write the rule once, let any pattern own it
Unidirectional flow is a constraint, not a framework: events come in, a transition decides the new state, effects run, results come back as events. The book's point is that this "can be implemented with a ViewModel, reducer, actor-owned store, coroutine state holder, TCA feature, Workflow, or custom state machine." Then: "The value is not the acronym; it is deterministic ownership and explicit effect boundaries."
enum CheckoutState: Sendable, Equatable {
case editing(Draft)
case submitting(operationID: String, Draft)
case outcomeUnknown(operationID: String, Draft)
case confirmed(bookingID: String)
}
enum CheckoutEvent: Sendable {
case intentPersisted(operationID: String)
case responseLost
case receiptArrived(bookingID: String)
}
// Pure: no I/O, no clock, no framework. A ViewModel, an MVI store
// or a TCA reducer can all call it, and one test covers all three.
func reduce(_ state: CheckoutState, _ event: CheckoutEvent) -> CheckoutState {
switch (state, event) {
case let (.editing(draft), .intentPersisted(id)):
return .submitting(operationID: id, draft)
case let (.submitting(id, draft), .responseLost):
return .outcomeUnknown(operationID: id, draft) // not .failed
case (.submitting, .receiptArrived(let booking)),
(.outcomeUnknown, .receiptArrived(let booking)):
return .confirmed(bookingID: booking)
default:
return state
}
}This changes the interview. Instead of defending MVVM against TCA, you show the rule that must hold whichever pattern hosts it, and the pattern becomes a decision about tooling, team familiarity and exit cost. Do not push every value through this loop either: text-field focus, animation phase and a disclosure toggle can stay next to the view.
Effects and lifetimes decide more than the pattern
Each effect needs an owner, a cancellation rule and a route for its result. A search request can be cancelled when the query changes. A durable booking submission must not be cancelled because a view disappeared: the screen stops observing, and an owner with a longer lifetime keeps the work.
This is where the platforms come in, and where a candidate shows depth without trivia. On iOS, main-actor isolation for UI state and actor-owned services for shared mutable state. On Android, a viewModelScope for feature UI work and WorkManager for durable, constraint-aware work. Neither MVVM nor TCA nor MVI decides those lifetimes for you. An answer that names the pattern and skips the lifetimes has answered the easy half.
One app, several patterns
Controlled Change runs one checkout through several patterns in a chapter it calls the pattern laboratory, and its fictional reference app ends up mixing them on purpose: search uses local state and a ViewModel, booking uses an explicit reducer and state machine, offline field work uses durable workflow state, and the application shell owns navigation and session composition. The book's conclusion: "Consistency comes from shared invariants for ownership, effects, and authority, not from one acronym."
That is a strong position to take in an interview, especially for a lead or staff role. Standardize the decision process, the integration boundaries, the lifecycle rules and the test expectations. Permit bounded variation where complexity differs. Mandating one pattern for every feature makes the simple screens pay the cost of the hardest one.
Price the exit before you adopt
Every pattern choice is also a future migration. Say how a feature would leave the pattern before you recommend it. For TCA the book is specific: because the coupling is deep, plan the exit at adoption and keep public feature contracts free of framework types, so a feature can leave without its callers changing. The same rule protects an MVI store or a Redux slice.
If the team is unsure, propose an experiment instead of a mandate. Implement one representative slice in two or three patterns and measure concepts and files, transition-test clarity, effect cancellation, restoration, onboarding time, changed lines for a new requirement and how much code is framework-specific. Use the difficult state, a response lost after commit, deep-link restoration or two concurrent edits, not a toy counter. Then pilot on one non-critical feature with real state pressure, with success and exit criteria written first.
And if the answer moves to migrating an existing feature from MVVM to a reducer or back, the book's playbook starts with a map of current state, actions and effects, and the actual failures, before anything is rewritten. Its warning applies to every pattern debate: watch for architecture migration as an identity campaign.
When the honest answer is the simple one
Sometimes the right answer is the least impressive one. If the scenario is an early product with two engineers, say so: one application module, simple state and a direct data boundary, with explicit states only where money or inventory already demand them. Direct state is not amateur architecture; it is a starting point with a named trigger for extracting more.
The book's chapter on system design interviews lists "choosing MVVM before requirements" among the common weak answers, and ends its advice on over-design in three words: "Restraint is senior behavior." For the same idea from the SwiftUI side, see SwiftUI vs UIKit: architecture is state ownership.
Mid, senior and staff: what the answers sound like
| Topic | Mid-level | Senior | Staff |
|---|---|---|---|
| First sentence | "MVVM, it's testable." | Asks what the feature and team look like. | Names the dominant pressure and what is out of scope. |
| State | "The ViewModel holds it." | One owner per piece of state, explicit unknown outcome. | Separates UI, durable intent and server authority, with lifetimes. |
| Pattern | One pattern everywhere. | Picks per feature, names its cost. | Standardizes invariants and the decision process, allows bounded variation. |
| Exit | Not mentioned. | Knows the pattern's failure signs. | Framework-free public contracts, a pilot with exit criteria. |
The staff column does not know more patterns. It knows when each one stops paying, and it leaves the team able to change its mind.
The follow-ups interviewers use to push
- "Where would you not use it?" Tests whether you can argue against your own choice.
- "The ViewModel is 2,000 lines." Tests splitting ownership instead of adding a layer.
- "A second team now edits this feature every sprint." Tests team pressure and boundaries.
- "The request timed out. Did the booking happen?" Tests the unknown outcome state.
- "We want to adopt TCA across the app." Tests pilot, exit and framework-free contracts.
- "It's a two-person startup." Tests restraint.
Prepare one sentence for each. If a follow-up makes you switch patterns, your first answer was a preference. If it makes you point at a pressure you already named, you are having the conversation the question is for.
Questions engineers ask about architecture pattern interviews
Which architecture should I say I would choose in a mobile interview?
Do not lead with a name. Ask about the feature, the team and the failure cases, name the pressure that dominates (state, ownership, failure, team, delivery or change), place state and effects, and only then pick a pattern for that feature. Close with what it costs and how a feature would leave it.
Is MVVM a weak answer?
No. MVVM fits forms, lists and workflows with straightforward transitions, and it is cheap to adopt and to leave. It becomes a weak answer when it is chosen before the requirements, or when the ViewModel turns into the place where navigation, networking, analytics and database code all end up.
Is TCA worth it?
It can be, for teams willing to adopt its model deeply and for features with rich state and effects, where exhaustive tests and composition pay. The cost is framework coupling and a learning curve, so plan the exit at adoption: keep public feature contracts free of framework types.
Can one app use several patterns?
Yes, and a strong answer says so. Simple screens keep local state, consequential flows get an explicit state machine, and consistency comes from shared rules for ownership, effects and authority rather than from one acronym everywhere.
