Mobile App Architecture Design
An improperly chosen architecture is not a "technical debt" in the abstract sense. It's a concrete situation six months later: you can't add push notifications without rewriting three screens because business logic lives directly in ViewController, and navigation is tied to AppDelegate. Designing architecture before development begins is an investment that pays off with the first significant requirement change.
Choosing a pattern: not MVVM "by default"
MVVM has become the de facto standard for both iOS (SwiftUI + Combine) and Android (Jetpack Compose + ViewModel), and Flutter (BLoC / Riverpod). But "setting up MVVM" without understanding the project's scope means overcomplicating a small app or under-structuring a large one.
What actually affects architecture choice:
- Team and experience. If the team hasn't worked with VIPER — don't implement VIPER. Learning a pattern in production costs more than its benefits.
- Scale and modularity. 5 screens — simple MVVM without Clean Architecture is sufficient. 50 screens with an 8-person team — Clean Architecture is mandatory, otherwise shared files cause conflicts and lack isolation.
- Testability requirements. VIPER and Clean Architecture provide better testability through dependency inversion, but require discipline from the entire team.
- Platform. iOS, Android, and Flutter have different patterns: VIPER is natural for iOS/UIKit, MVP is historically popular on Android with MVP libraries, BLoC became the standard for Flutter.
What architecture design includes
It's not a diagram in Miro with rectangles labeled "Model — View — ViewModel". Full-fledged architecture design includes:
Layering. Define layer boundaries: Presentation / Domain / Data. Document rules: the domain layer doesn't know about Flutter/UIKit, the data layer doesn't know about a specific UI framework. Dependency Rule — dependencies only inward, never outward.
Modularization. For large projects: split into feature modules (auth, profile, payments, feed). On iOS — Swift Package Manager or cocoapods submodules. On Android — Gradle multi-module. On Flutter — Dart packages within a monorepo. Modularity enables parallel development and manageable build times.
Dependency Injection strategy. Choose a DI container: iOS — Resolver / Swinject / manual clean DI, Android — Hilt (Dagger2 with code generation), Flutter — GetIt + injectable. Design the dependency graph, define object lifetimes (singleton / scoped / transient).
Navigation. iOS: UINavigationController / Coordinator pattern / SwiftUI NavigationStack. Android: Jetpack Navigation Component with NavGraph. Flutter: go_router with deep linking. The Coordinator pattern is important with complex navigation involving conditions (authorized / not authorized / onboarding not completed).
State management strategy. Not just "we use Riverpod" — but how exactly: where global state lives (user, theme, locale), where local state lives (specific screen state), how errors are handled (AsyncValue.error), how side effects are separated.
Network layer. Retrofit (Android) / Moya (iOS) / Dio (Flutter). Interceptors for authorization (token refresh), logging, retry logic. Error handling: map HTTP codes to domain errors, don't throw DioException into the presentation layer.
Offline strategy. Do you need offline support at all? If yes: Room (Android) / Core Data / SwiftData (iOS) / drift or Hive (Flutter). Repository pattern as abstraction over remote + local data sources: read from cache first, update from network in the background.
Architecture documentation
The work result is not just a working scaffold. Document as:
- ADRs (Architecture Decision Records) — why you chose this pattern, what alternatives you considered
- Component and dependency diagrams (C4 model: Context → Container → Component)
- Coding conventions and examples for each layer
- Template feature module structure that the entire team follows
Practical case
A Flutter logistics app (30+ screens, real-time tracking, offline driver support) initially had "BLoC everywhere without clear layer separation". After 4 months: BLoC called Dio directly, tests weren't written (impossible to isolate), adding new features required changes in 5–7 unrelated files.
Refactoring to Clean Architecture with GetIt + injectable, go_router, drift for offline took 3 weeks, but the next 6 months the team added features twice as fast.
Comparison of approaches
| Criterion | MVC/MVP | MVVM | Clean Architecture |
|---|---|---|---|
| Entry barrier | Low | Medium | High |
| Testability | Low | Medium | High |
| Team size | 1–2 people | 2–5 people | 5+ people |
| Design time | 0.5 day | 1 day | 3–5 days |
| ROI | From first month | From 3rd month | From 6th month |
Process and timeline
- Requirements audit — functional and non-functional requirements, NFR for performance and offline
- Platform and stack analysis — iOS / Android / Flutter / cross-platform, existing code if any
- Design — pattern selection, decision documentation, ADR
- Scaffold implementation — basic project structure with examples for each layer
- Code review and onboarding — team explanation of principles, code review of first features
Architecture design: 3–5 days. For legacy refactoring with existing code — more. Cost is calculated after analyzing scalability requirements and team composition.







