Architecture¶
What is MVVM in Android architecture?¶
View Answer
MVVM separates UI rendering from business/data orchestration.
Typical split:
-
View: renders state and forwards user actions
-
ViewModel: exposes lifecycle-aware UI state
-
Repository: abstracts data access
Why teams use it:
-
better testability
-
cleaner separation of concerns
-
easier lifecycle-safe state handling
What is the role of a ViewModel in scalable Android apps?¶
View Answer
ViewModel owns screen-level state and survives configuration changes.
Key responsibilities:
-
transform domain data into UI-friendly state
-
coordinate use cases/repositories
-
expose immutable observable state
-
handle user intents/events
It should avoid direct Android UI references.
When should you use SavedStateHandle in architecture design?¶
View Answer
Use SavedStateHandle for small, restorable screen state
tied to process recreation.
Good candidates:
-
selected tab/index
-
filter/sort selection
-
lightweight form progress
Avoid storing large objects or domain caches in it.
What is MVI architecture?¶
View Answer
MVI models UI as a loop: intent -> reducer -> new immutable state.
Core benefits:
-
predictable state transitions
-
easier debugging/time-travel style reasoning
-
explicit event handling contract
Tradeoff: can introduce boilerplate if over-applied.
MVVM vs MVI - how do you choose?¶
View Answer
Choose by complexity and team needs, not trend preference.
MVVM:
-
lower ceremony
-
common Android default
MVI:
-
stronger state/event determinism
-
better for complex interaction-heavy screens
Many teams use MVVM + UDF patterns as a middle ground.
What are the key principles of Unidirectional Data Flow?¶
View Answer
UDF means state flows down, events flow up.
Interview-ready points:
-
single source of truth for screen state
-
immutable state exposure
-
explicit intent/event handlers
-
deterministic state transitions
This reduces hidden mutation bugs.
What is Clean Architecture in Android?¶
View Answer
Clean Architecture organizes code by responsibility and dependency direction.
Typical layers:
-
presentation
-
domain
-
data
Main rule: inner layers should not depend on outer framework details.
What is the dependency rule in layered architecture?¶
View Answer
Dependencies must point inward toward stable policy layers.
Practical implications:
-
UI depends on domain contracts
-
data implements domain abstractions
-
domain avoids Android/framework types
This improves portability and test isolation.
How does dependency inversion apply to Android app architecture?¶
View Answer
High-level policies should depend on abstractions, not concrete SDK/data details.
In Android this usually means:
-
domain uses interfaces
-
data/network/db implement those interfaces
-
DI wires concrete implementations
It keeps business logic resilient to infrastructure changes.
Why use the Repository pattern?¶
View Answer
Repository abstracts data origin and provides a clean API to higher layers.
Benefits:
-
decouples UI/domain from network/db details
-
centralizes data policies
-
simplifies testing with fakes
-
enables caching/sync orchestration
How does a repository support a Single Source of Truth model?¶
View Answer
Repository enforces one authoritative read path for consumers.
Common approach:
-
local DB is canonical source
-
network refresh updates DB
-
UI observes DB-backed streams
This avoids competing data sources in UI.
How should repositories orchestrate network, cache, and database sources?¶
View Answer
Define explicit policies for freshness, fallback, and write ordering.
Typical strategy:
-
read local first
-
fetch remote by staleness rules
-
merge/validate payload
-
persist then emit
Keep these rules in repository, not UI.
What problem do use cases solve in architecture?¶
View Answer
Use cases encapsulate business actions independent of UI and data frameworks.
They help by:
-
isolating domain logic
-
improving reuse across screens
-
making behavior unit-testable
-
reducing ViewModel complexity
How granular should use cases be?¶
View Answer
Use cases should represent coherent business actions, not tiny wrappers.
Guidance:
-
too coarse: hard to compose/test
-
too fine: boilerplate and indirection
-
optimize for domain clarity and change boundaries
Granularity is a context-based design decision.
When is a dedicated domain layer worth adding?¶
View Answer
Add domain layer when business rules are non-trivial or reused.
Signals:
-
multiple features share logic
-
policies outgrow ViewModels
-
testability/portability requirements increase
For simple apps, extra layers can be unnecessary overhead.
Why is dependency injection important in Android architecture?¶
View Answer
DI manages object creation/wiring outside business logic.
Benefits:
-
loose coupling
-
easier testing with fakes/mocks
-
centralized lifecycle/scope control
-
clearer dependency graph at scale
Constructor injection vs field injection - which is preferred?¶
View Answer
Constructor injection is usually preferred.
Reasons:
-
explicit required dependencies
-
easier immutable object design
-
better unit-test ergonomics
Field injection is useful in framework-controlled classes but less explicit.
How do DI scopes affect memory and lifecycle behavior?¶
View Answer
Scopes define object lifetime and reuse boundaries.
Mis-scoping can cause:
-
leaks (too long-lived)
-
churn/perf cost (too short-lived)
-
inconsistent shared state
Align scopes with Activity/Fragment/ViewModel/app lifetimes.
What architectural advantages does Hilt provide?¶
View Answer
Hilt standardizes DI setup for Android component lifecycles.
Key advantages:
-
less boilerplate than manual Dagger setup
-
predefined component hierarchy
-
easier onboarding and consistency
-
strong integration with ViewModel/WorkManager
What Hilt component lifetimes should senior engineers know?¶
View Answer
Senior interviews expect understanding of scope boundaries.
Common lifetimes:
-
SingletonComponent: app-wide
-
ActivityRetainedComponent: across config changes
-
ViewModelComponent: ViewModel lifetime
-
Activity/Fragment components: UI-bound
Wrong scope choices create subtle bugs.
Dagger vs Hilt - what is the architectural tradeoff?¶
View Answer
Hilt is Dagger with Android-focused conventions and generated glue code.
Tradeoff framing:
-
Hilt: faster setup, consistent patterns
-
raw Dagger: maximum graph/control flexibility
Choose based on customization needs and team productivity.
What should you understand about Dagger components and subcomponents?¶
View Answer
Components define object graph roots; subcomponents model child lifecycles.
Important points:
-
parent can provide bindings to child
-
child can narrow scope/lifetime
-
graph shape affects compilation and maintainability
Avoid overly complex graph hierarchies.
What are Dagger/Hilt build and runtime tradeoffs at scale?¶
View Answer
DI frameworks improve structure but add compile-time and graph complexity costs.
Staff-level concerns:
-
annotation processing build impact
-
module graph growth over time
-
debugging generated binding errors
-
balancing explicitness vs velocity
What is a Service Locator pattern?¶
View Answer
Service Locator is a registry that provides dependencies on demand.
It can simplify small systems but often hides real dependencies.
Interview angle:
-
quick bootstrap option
-
weaker explicitness than constructor DI
-
can reduce testability if overused
Service Locator vs DI - why does this matter in interviews?¶
View Answer
DI makes dependencies explicit in constructors; Service Locator hides them at call sites.
Consequences:
-
DI is easier to reason about and test
-
Service Locator can create implicit coupling
-
hidden runtime failures are more likely
Prefer DI for medium/large apps.
Why modularize Android apps?¶
View Answer
Modularization separates code by ownership and change boundaries.
Benefits:
-
faster incremental builds
-
clearer feature boundaries
-
parallel team development
-
safer refactoring and release isolation
What multi-module structures are common in Android?¶
View Answer
Common structures include layered modules and feature-first modules.
Typical options:
-
app + core + feature modules
-
domain/data/presentation split per feature
-
hybrid platform modules for shared infra
Choose structure based on team and product complexity.
How do API vs implementation module boundaries improve architecture?¶
View Answer
Expose only stable contracts and keep internals hidden.
Benefits:
-
reduced coupling and accidental usage
-
clearer ownership contracts
-
better compile isolation
-
safer internal refactors
Public surface area should stay intentionally small.
What defines a good feature module boundary?¶
View Answer
A good boundary aligns with user-facing capabilities and team ownership.
Signs of healthy boundaries:
-
minimal cross-feature dependencies
-
explicit navigation/contracts
-
isolated tests and release paths
-
clear domain language per feature
When should you use dynamic feature modules?¶
View Answer
Use dynamic delivery when features are optional, heavy, or region-specific.
Tradeoffs:
-
better install size/startup profile
-
added delivery/testing complexity
-
more runtime handling for missing modules
Evaluate product value against operational cost.
How should dependency direction work between feature modules?¶
View Answer
Features should depend on shared contracts, not each other's internals.
Recommended pattern:
-
keep cross-feature APIs contract-driven
-
avoid cyclic feature dependencies
-
place shared infra in core/platform modules
This preserves build and team independence.
What is a strong state management approach in Android architecture?¶
View Answer
Keep state ownership explicit and state models immutable where possible.
Common approach:
-
ViewModel owns screen state
-
UI renders state + emits events
-
repository/domain updates source state
Avoid scattered mutable flags across layers.
What does Single Source of Truth mean in practice?¶
View Answer
One authoritative state source should drive reads for a given data set.
In practice:
-
define canonical owner (often DB-backed)
-
route all writes through controlled paths
-
keep projections/derived views read-only
This reduces inconsistency and race conditions.
Why model UI state as immutable data classes?¶
View Answer
Immutable state makes transitions explicit and easier to test.
Benefits:
-
predictable rendering behavior
-
simpler equality/change reasoning
-
safer concurrent/reactive usage
-
clearer reducer-style updates
What is offline-first architecture?¶
View Answer
Offline-first treats local storage as primary read path, syncing with network opportunistically.
Core principles:
-
local-first reads
-
resilient queued writes
-
explicit conflict policy
-
sync observability/telemetry
Push, pull, and hybrid sync strategies - when to use each?¶
View Answer
Choose strategy by freshness needs, battery budget, and backend capability.
Quick framing:
-
pull: simple, periodic consistency
-
push: lower latency updates
-
hybrid: practical balance for many products
Design for retries and backoff from day one.
How should architecture handle sync conflicts?¶
View Answer
Conflict policy must be explicit and domain-aware.
Common strategies:
-
last write wins (simple, risky)
-
version/vector based merge
-
server-authoritative with client reconciliation
-
user-assisted conflict resolution for critical entities
Track conflict metrics to tune policy.
What caching strategies are common in Android architecture?¶
View Answer
Choose cache strategy per data volatility and UX expectations.
Common options:
-
memory cache for hot short-lived reads
-
disk/DB cache for persistence
-
TTL or staleness-based refresh
-
cache-aside or network-bound-resource style
What does a robust pagination architecture look like?¶
View Answer
Robust pagination handles loading state, errors, deduplication, and persistence.
Must-have concerns:
-
stable page keys/cursors
-
append/prepend retry behavior
-
local cache coherence
-
refresh invalidation strategy
How does StateFlow fit Android architecture design?¶
View Answer
StateFlow is a lifecycle-friendly state stream for UI layers.
Architectural usage:
-
ViewModel exposes immutable
StateFlow<UiState> -
UI collects and renders
-
updates come from repository/use-case pipelines
Keep one-off events separate from persistent state.
How should one-off events be handled in reactive architecture?¶
View Answer
Model state and events as different channels.
Typical pattern:
-
StateFlowfor persistent UI state -
SharedFlow/Channel for one-time events -
consume events with lifecycle awareness
Avoid encoding transient events as sticky state flags.
What is a good error handling architecture for Android apps?¶
View Answer
Handle errors by layer and map them to domain/UI semantics.
Recommended approach:
-
classify technical vs business errors
-
normalize in repository/domain boundaries
-
expose user-actionable UI states
-
log structured diagnostics for operations
How do retry strategies fit architecture decisions?¶
View Answer
Retry policy should be explicit, bounded, and context-aware.
Common rules:
-
exponential backoff for transient failures
-
idempotency awareness for writes
-
user-driven retry for recoverable UI actions
-
circuit-breaker style guardrails for unstable backends
How should complex UI state be modeled architecturally?¶
View Answer
Use explicit immutable state models with clear sub-states.
Common shape:
-
loading/content/error/empty branches
-
data payload + UI metadata
-
separate ephemeral events
Prefer readability and deterministic transitions over cleverness.
What are key principles of navigation architecture?¶
View Answer
Navigation should use explicit destination contracts and clear ownership.
Good practices:
-
central route definitions
-
pass IDs/contracts, not large mutable objects
-
keep navigation decisions near state owner
-
test back stack behavior for critical flows
How should deep links be designed in modular Android apps?¶
View Answer
Treat deep links as stable external API contracts.
Architecture implications:
-
central validation and parsing
-
module-level routing boundaries
-
auth/feature-flag gating support
-
backward compatibility strategy
How do you design Android architecture for high testability?¶
View Answer
Testability improves when dependencies and state transitions are explicit.
Design choices:
-
constructor-injected abstractions
-
pure use-case logic where possible
-
deterministic state reducers
-
contract tests at module boundaries
How does architecture impact team scalability?¶
View Answer
Architecture defines ownership boundaries, release independence, and coordination cost.
Team-scale signals:
-
clear module ownership
-
low cross-team dependency hotspots
-
predictable integration contracts
-
standards for observability and quality gates
What is architecture governance in large Android codebases?¶
View Answer
Governance is how teams enforce architectural direction without blocking delivery.
Typical mechanisms:
-
module ownership model
-
ADRs and decision logs
-
lint/static checks for boundaries
-
review standards and architecture forums
How should senior engineers discuss architecture tradeoffs in interviews?¶
View Answer
Frame tradeoffs by context, constraints, and measurable outcomes.
Strong answer structure:
-
what problem and constraints existed
-
options considered
-
decision and rationale
-
risks/mitigations
-
follow-up metrics and iteration
Avoid presenting architecture as one-size-fits-all.
Compare DataStore vs SharedPreferences - consistency, migration, and threading model¶
View Answer
DataStore replaces SharedPreferences with a fully asynchronous, coroutine-based API that uses atomic file operations to prevent partial writes and data corruption.
In interviews, cover:
-
SharedPreferences is synchronous in commit() (ANR risk on large writes) or fire-and-forget in apply() (crash on process kill before write completes); reads are synchronous and block the calling thread if the file hasn't been loaded
-
DataStore (Preferences DataStore) exposes Flow
; all reads and writes are non-blocking coroutine operations on an IO-backed dispatcher; no more synchronous reads on the main thread -
Atomic writes via an atomic rename (write to temp file then rename) prevent partial write corruption โ a process kill mid-write still yields the last valid file
-
Proto DataStore uses protobuf for typed key schema; Preferences DataStore uses untyped key objects; both live in a single-process single-file design (no cross-process access)
-
migration: ReplaceFileCorruptionHandler, SharedPreferencesMigration; migrate incrementally by creating DataStore with the old SP file name and letting it auto-import
Strong answer tip:
- never mix SharedPreferences and DataStore on the same file path; DataStore acquires a FileLock and SP does not โ they will corrupt each other if both access the same file
Explain Room + Flow invalidation - correctness problems and avoiding over-collection¶
View Answer
Room's Flow-based queries re-emit on every table write that affects the observed table, not just writes that change the result set โ this causes unnecessary recompositions when not handled correctly.
In interviews, cover:
-
Room Flow is backed by InvalidationTracker; any write to a table observed by a query triggers re-emission, even if the query result is identical โ this can cause high-frequency re-renders on write-heavy tables
-
fix: apply distinctUntilChanged() downstream to suppress equal consecutive emissions
-
collecting inside the ViewModel using stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList()): shares the upstream database flow while there are active UI subscribers and caches the last value for 5 seconds after the last subscriber disconnects โ prevents restart on rotation
-
do NOT collect Room Flow in a simple LaunchedEffect without cancellation management; the flow continues emitting even when the composable leaves composition unless the scope is tied to the composable lifecycle
Strong answer tip:
- benchmark with invalidation tracking disabled and enabled on a write-heavy table; the difference shows whether your architecture is over-collecting or correctly throttling
Explain Room transactions, WAL mode, and concurrency correctness under load¶
View Answer
Room uses SQLite under the hood; WAL mode allows concurrent reads while a write is in progress, but transaction boundaries and thread safety must still be managed explicitly.
In interviews, cover:
-
@Transaction on a @Dao method executes the body inside a single SQL transaction; required for multi-table writes that must be atomic (e.g. insert order + insert items in one atomic operation)
-
WAL (Write-Ahead Logging): readers do not block writers and writers do not block readers in WAL mode; Room enables WAL by default since version 2.4; improves throughput on read-heavy workloads
-
concurrency: Room queues writes on a single-threaded executor by default; multiple coroutines doing concurrent writes will serialize; for high write throughput, design batched inserts rather than many small single-row writes
-
never use @Transaction with very long-running operations (network calls, slow computations) โ this holds an exclusive write lock and starves reads
Strong answer tip:
- test with Room's testing support: RoomDatabase.inMemoryDatabaseBuilder() combined with a custom QueryCallback to assert the SQL generated by your DAO matches expected index usage
Explain sync engine design - idempotency, deduplication, and at-least-once semantics¶
View Answer
A robust sync engine guarantees that every user action eventually reaches the server exactly once โ achieved through idempotent operations, at-least-once delivery with server-side deduplication.
In interviews, cover:
-
at-least-once delivery: the client retries until it receives a server acknowledgement; the server must deduplicate to prevent double application of the operation
-
idempotency key: each client-generated operation carries a UUID; the server stores processed IDs and rejects re-submissions without error (returns the original response)
-
outbox pattern: write operations go to a local outbox table first (atomic with the UI change); WorkManager drains the outbox; on 2xx response, delete the outbox row; on retryable error, backoff and retry; on permanent failure, tombstone the row and surface to UX
-
deduplication window: server must retain idempotency keys for at least the maximum expected retry window (typically 7โ30 days)
Strong answer tip:
- ordering matters for operations that depend on each other (e.g. create comment requires post to exist first); tag operations with a dependency chain and ensure the drainer respects it
Explain deletes and tombstones in offline sync - preventing resurrected data¶
View Answer
In offline-first apps, deleting a record locally then syncing risks the delete being lost if another device re-syncs the same record after the delete โ tombstones prevent this resurrection.
In interviews, cover:
-
resurrection problem: client A deletes record; client B (offline) has the record; client B comes online and upserts the record โ without a tombstone, the record reappears on client A after next sync
-
tombstone: a deleted_at timestamp replaces the full delete; the sync layer treats records with deleted_at set as soft-deleted; all clients respect deleted_at and hide the record
-
garbage collection: tombstones must be retained for the maximum expected offline window (e.g. 30 days); after that, hard-delete the tombstone and the record permanently
-
conflict resolution: if both a delete and an update arrive for the same record, last-writer-wins is dangerous; prefer explicit policy โ e.g. update wins over delete (data preservation) or delete wins over update (GDPR compliance)
Strong answer tip:
- for GDPR right-to-erasure compliance, hard deletion must be certain; log the deletion event, replicate it to all downstream stores, and verify through compliance audit that no copies remain in backups or caches