ADR Reference: ADR-MOB-001
The architecture pattern is the highest-leverage structural decision in mobile engineering. It determines whether business logic is testable in isolation, whether security properties are auditable, and whether the codebase can scale across teams and years without accumulating irreversible structural debt.
| Field |
Value |
| ADR Reference |
ADR-MOB-001 |
| Version |
1.0 |
| Date Raised |
May 2025 |
| Review Date |
November 2025 |
| Author |
Solutions Architecture Practice — Ascendion |
| Status |
ACCEPTED |
| Domain |
Mobile Architecture |
| ARB Approval |
Required |
| Stakeholders |
Mobile Engineering Leads · Solutions Architects · Security Architecture · QA Lead · Product Management · Client CTO |
Executive Summary
Decision: Clean Architecture combined with MVVM is adopted as the primary architectural pattern for all native mobile applications delivered by Ascendion. On Android: Jetpack Compose, ViewModel, Repository, Use Cases, Hilt for dependency injection. On iOS: SwiftUI, @Observable, Repository, Use Cases, Factory DI. MVVM is adopted over MVI, MVP, VIPER, and TCA based on weighted evaluation across security, performance, development speed, testability, and long-term maintainability.
Decision Drivers
| Priority |
Quality Attribute |
Weight |
Rationale |
| 1 |
Security |
25% |
Financial services mandate security-by-architecture. Business logic isolation is a compliance requirement. |
| 2 |
Testability |
20% |
≥ 80% unit test coverage on business logic is a mandatory delivery gate. |
| 3 |
Performance |
18% |
Client SLAs: < 2s cold start, 60fps rendering, < 100ms UI response. |
| 4 |
Development Speed |
17% |
Clear conventions reduce decision fatigue and accelerate onboarding. |
| 5 |
Maintainability |
12% |
Mobile applications have 3–5 year lifespans. Architecture must accommodate this. |
| 6 |
Cost Optimisation |
8% |
Shared patterns reduce cross-team knowledge transfer cost. |
Considered Options
Option A — Clean Architecture + MVVM (ADOPTED)
Three concentric rings with inward-only dependency rule. Entities (domain models, zero platform dependencies), Use Cases (business rules, returns Result types), Interface Adapters (ViewModel, Repository implementations), Frameworks & Drivers (outermost — Android SDK, UIKit, SwiftUI). ViewModel exposes StateFlow (Android) and @Observable (iOS). Testable at every layer without device or network. Weighted score: 4.62 / 5.0
Option B — MVI (Model-View-Intent)
Unidirectional data flow with Intent → Reducer → State → View. Excellent for complex interdependent state. Higher boilerplate. iOS ecosystem support weak at current maturity. Weighted score: 3.62 / 5.0
Option C — MVP
Presenter is not lifecycle-aware — superseded by ViewModel in 2018. Not permitted for new projects. Legacy MVP codebases must document a migration plan. Weighted score: 2.96 / 5.0 — DISMISSED
Option D — VIPER
iOS-only. Five files per screen. Poor SwiftUI compatibility. Cannot share pattern knowledge with Android team. Weighted score: 3.72 / 5.0
Option E — TCA (The Composable Architecture)
Best-in-class iOS testability. iOS-only — no Android equivalent. 4–6 week onboarding for new engineers. Third-party dependency risk. Weighted score: 3.61 / 5.0
Decision
Clean Architecture + MVVM is mandatory for all new native mobile projects from May 2025. The Dependency Rule is non-negotiable: all source code dependencies point inward. Domain layer must never import Presentation or Data layers.
Mandatory Security Rules:
1. ALL network calls originate from the Data layer only — network call in ViewModel is a blocking code review finding
2. Credentials: Android Keystore + EncryptedSharedPreferences; iOS Keychain with kSecAttrAccessibleWhenUnlockedThisDeviceOnly
3. OAuth 2.0 + PKCE is the only permitted authentication flow
4. All PII fields annotated with @Pii (Kotlin) or @Masked (Swift) — stripped from logs by ProGuard rule
Mandatory Test Coverage:
| Layer |
Target |
Tooling |
| Use Cases |
≥ 90% line coverage |
JUnit5 + Mockk / XCTest + Swift Testing |
| ViewModels |
≥ 80% line coverage |
Turbine (Flow) / Combine-schedulers |
| Repository |
Integration tests per method |
Room in-memory / URLProtocol stub |
| UI Components |
Snapshot all states |
Paparazzi / iOSSnapshotTestCase |
Trade-off Analysis
| Trade-off Accepted |
Consequence |
Mitigation |
| Boilerplate per screen |
A simple read screen requires entity, interface, impl, Use Case, ViewModel, View |
Accepted explicitly. Discipline prevents spaghetti patterns that destroy maintainability at scale. |
| DI configuration complexity |
Hilt module graph requires understanding for new engineers |
Golden path scaffolding generates boilerplate automatically. 1-day onboarding target. |
| Kotlin/Swift language split |
No shared UI code between Android and iOS |
Shared domain logic via KMP where teams are large enough to justify it. |
Implementation Guidance
- Generate new feature modules using the golden path scaffold — do not copy existing modules
- Domain layer classes are in separate Gradle/SPM modules with no Android SDK or UIKit imports
- ViewModel maximum 200 lines — mandatory ARB review if exceeded
- Use Case has a single execute() method returning Result
- Repository interface defined in domain module; implementation in data module
- StateFlow for ongoing state; SharedFlow with replay=0 for one-time navigation events
- JaCoCo (Android) and Xcode Coverage (iOS) gates enforced in CI before PR merge
Compliance Checkpoints
| Checkpoint |
Trigger |
Owner |
SLA |
| Architecture Pattern Review |
Every new mobile project kickoff |
Solutions Architect |
Before sprint 1 |
| Layer Boundary Violation |
Any PR with network call in ViewModel |
Tech Lead code review |
Block PR merge |
| Coverage Gate |
Every PR — automated CI |
CI Pipeline |
Real-time |
| ViewModel Size Review |
Any ViewModel exceeding 200 lines |
ARB |
Within 5 business days |
| Security Pattern Audit |
Before first production release |
Security Architect |
Release gate |
| Reference |
Title |
Relationship |
| ADR-MOB-002 |
Mobile Platform Selection |
Platform governs which technology implements this pattern |
| ADR-MOB-003 |
Mobile CI/CD Pipeline |
Test coverage gates enforced by CI reference this ADR |
| ADR-SEC-011 |
Mobile Security Controls |
Security controls designed around the layer boundaries defined here |
| ADR-INT-005 |
BFF API Design |
Repository interface design maps to BFF endpoints |
Flowchart
%%{init:{'theme':'base','themeVariables':{'fontSize':'14px','fontFamily':'IBM Plex Sans, system-ui, sans-serif','primaryColor':'#DBEAFE','primaryTextColor':'#1e3a5f','primaryBorderColor':'#2563EB','lineColor':'#374151','clusterBkg':'#F9FAFB','clusterBorder':'#D1D5DB','edgeLabelBackground':'#FFFFFF'},'flowchart':{'curve':'orthogonal','padding':30,'nodeSpacing':65,'rankSpacing':75,'useMaxWidth':true}}}%%
flowchart TD
subgraph PRESENTATION["🖥 Presentation Layer — Outermost Ring — Platform Specific"]
direction LR
VIEW_A["<b>View — Android</b><br/>Jetpack Compose<br/>collectAsStateWithLifecycle<br/>No business logic"]
VIEW_I["<b>View — iOS</b><br/>SwiftUI Views<br/>task + onChange modifiers<br/>No business logic"]
VM_A["<b>ViewModel — Android</b><br/>androidx.lifecycle.ViewModel<br/>UiState: StateFlow<br/>viewModelScope coroutines"]
VM_I["<b>ViewModel — iOS</b><br/>@Observable class<br/>MainActor isolated<br/>async/await Tasks"]
end
subgraph DOMAIN["🏛 Domain Layer — Zero Platform Imports — Most Stable"]
direction LR
UC_D["<b>Use Cases</b><br/>One per business operation<br/>Single execute() method<br/>Returns Result<T, DomainError>"]
RI_D["<b>Repository Interfaces</b><br/>Protocol/Interface only<br/>No implementations<br/>Domain owns the contract"]
DM_D["<b>Domain Models</b><br/>data class · struct<br/>BigDecimal for monetary values<br/>Zero Android/iOS imports"]
end
subgraph DATA["🗄 Data Layer — Interface Adapters"]
direction LR
RIMPL_D["<b>Repository Implementations</b><br/>Implements domain interface<br/>Cache strategy decision<br/>Maps DTO → Domain Entity"]
REMOTE_D["<b>Remote DataSource</b><br/>Retrofit · OkHttp (Android)<br/>URLSession (iOS)<br/>DTO models only"]
LOCAL_D["<b>Local DataSource</b><br/>Room + SQLCipher (Android)<br/>SwiftData FileProtection (iOS)<br/>Offline queue + cache"]
CRED_D["<b>Credential Storage</b><br/>Android Keystore<br/>iOS Keychain + SecureEnclave<br/>Access token — memory only"]
end
subgraph COVERAGE["✅ Mandatory Test Coverage — CI Gates"]
direction LR
UC_COV["<b>Use Case Coverage</b><br/>≥ 90% line coverage<br/>JUnit5 + Mockk<br/>XCTest + Swift Testing"]
VM_COV["<b>ViewModel Coverage</b><br/>≥ 80% line coverage<br/>Turbine (Flow)<br/>Combine-schedulers"]
SNAP["<b>Snapshot Tests</b><br/>Paparazzi (Android)<br/>iOSSnapshotTestCase<br/>All themes · All Dynamic Type"]
end
VIEW_A & VIEW_I -->|"User Events"| VM_A & VM_I
VM_A & VM_I -->|"UiState"| VIEW_A & VIEW_I
VM_A & VM_I -->|"executes"| UC_D
UC_D -->|"calls interface"| RI_D
RI_D --> RIMPL_D
RIMPL_D --> REMOTE_D & LOCAL_D & CRED_D
UC_D -.->|"90% coverage gate"| UC_COV
VM_A & VM_I -.->|"80% coverage gate"| VM_COV
VIEW_A & VIEW_I -.->|"snapshot gate"| SNAP
style PRESENTATION fill:#DBEAFE,stroke:#2563EB,stroke-width:2px
style DOMAIN fill:#DCFCE7,stroke:#16A34A,stroke-width:2px
style DATA fill:#FEF3C7,stroke:#D97706,stroke-width:2px
style COVERAGE fill:#F3E8FF,stroke:#7C3AED,stroke-width:2px
style VIEW_A fill:#BFDBFE,stroke:#2563EB,color:#1e3a5f
style VIEW_I fill:#BFDBFE,stroke:#2563EB,color:#1e3a5f
style VM_A fill:#93C5FD,stroke:#1D4ED8,color:#1e3a5f
style VM_I fill:#93C5FD,stroke:#1D4ED8,color:#1e3a5f
style UC_D fill:#BBF7D0,stroke:#15803D,color:#14532D
style RI_D fill:#BBF7D0,stroke:#15803D,color:#14532D
style DM_D fill:#86EFAC,stroke:#16A34A,color:#14532D
style RIMPL_D fill:#FDE68A,stroke:#D97706,color:#78350F
style REMOTE_D fill:#FDE68A,stroke:#D97706,color:#78350F
style LOCAL_D fill:#FDE68A,stroke:#D97706,color:#78350F
style CRED_D fill:#FCA5A5,stroke:#DC2626,color:#7f1d1d
style UC_COV fill:#E9D5FF,stroke:#7C3AED,color:#4C1D95
style VM_COV fill:#E9D5FF,stroke:#7C3AED,color:#4C1D95
style SNAP fill:#E9D5FF,stroke:#7C3AED,color:#4C1D95
References
- Martin, Robert C. — Clean Architecture. Prentice Hall, 2017.
- Google — Android App Architecture Guide. developer.android.com/topic/architecture
- Google — Now in Android (reference implementation). github.com/android/nowinandroid
- Point-Free — The Composable Architecture. github.com/pointfreeco/swift-composable-architecture
- Fowler, Martin — Patterns of Enterprise Application Architecture. Addison-Wesley, 2002.
⚠ God ViewModel — ViewModel exceeds 200 lines handling networking, business logic, navigation, analytics, and error handling simultaneously. Untestable and a constant merge conflict source in teams larger than 3.
CORRECT: Business logic in Use Cases. Navigation events as a sealed class. Each concern in its own class. ARB review triggers mandatory refactor before the PR is merged.
⚠ Network Call in ViewModel — viewModelScope.launch { val data = apiService.get() } in a ViewModel class. Security boundary violated. Untestable without mocking the entire network stack.
CORRECT: ViewModel calls Use Case. Use Case calls Repository interface. Repository implementation calls API client. Each layer is independently testable.
⚠ Domain Models with Android Annotations — @Entity data class Account(...) used as both the Room database entity and the domain model. Imports Room into the domain layer, violating the Dependency Rule.
CORRECT: Separate domain models (data class Account) from data layer entities (@Entity data class AccountEntity). Repository implementations map between them.
End of Document
Maintained by
Architecture Council
Review Cadence
Annually or upon any
Tier 2 architectural change
Change Requests
Submitted as RFC to the
Architecture Council for review
Version History
Maintained in version
control alongside this document