ESC
Type to search guides, tutorials, and reference documentation.
Verified by Garnet Grid

Mobile App Architecture Patterns

Choose and implement the right architecture pattern for your mobile application. Covers MVC, MVVM, Clean Architecture, Coordinator pattern, dependency injection, and how architecture choices affect testability, maintainability, and team velocity.

Mobile app architecture determines how maintainable your codebase will be at 50,000 lines, 100,000 lines, and beyond. The right architecture separates concerns cleanly, makes testing straightforward, and allows multiple developers to work in parallel without conflicts.


Architecture Comparison

PatternTestabilityComplexityBest For
MVCLowLowPrototypes, simple apps
MVPMediumMediumForms-heavy apps
MVVMHighMediumData-driven UIs
Clean ArchitectureVery HighHighLarge-scale, long-lived apps
MVI/ReduxVery HighHighComplex state management

MVVM (Model-View-ViewModel)

The most popular production architecture for iOS and Android:

View (SwiftUI/Compose)
  ↓ Observes
ViewModel (Business Logic)
  ↓ Calls
Repository (Data Abstraction)
  ↓ Fetches
Data Sources (API, Database, Cache)

iOS (SwiftUI)

class OrderListViewModel: ObservableObject {
    @Published var orders: [Order] = []
    @Published var isLoading = false
    @Published var error: String?
    
    private let repository: OrderRepository
    
    init(repository: OrderRepository = OrderRepositoryImpl()) {
        self.repository = repository
    }
    
    @MainActor
    func loadOrders() async {
        isLoading = true
        error = nil
        
        do {
            orders = try await repository.getOrders()
        } catch {
            self.error = error.localizedDescription
        }
        
        isLoading = false
    }
}

struct OrderListView: View {
    @StateObject private var viewModel = OrderListViewModel()
    
    var body: some View {
        List(viewModel.orders) { order in
            OrderRow(order: order)
        }
        .overlay {
            if viewModel.isLoading { ProgressView() }
        }
        .task { await viewModel.loadOrders() }
    }
}

Android (Jetpack Compose)

class OrderListViewModel(
    private val repository: OrderRepository
) : ViewModel() {
    
    private val _uiState = MutableStateFlow<OrderListState>(OrderListState.Loading)
    val uiState: StateFlow<OrderListState> = _uiState.asStateFlow()
    
    init { loadOrders() }
    
    private fun loadOrders() {
        viewModelScope.launch {
            _uiState.value = OrderListState.Loading
            repository.getOrders()
                .onSuccess { orders ->
                    _uiState.value = OrderListState.Success(orders)
                }
                .onFailure { error ->
                    _uiState.value = OrderListState.Error(error.message)
                }
        }
    }
}

sealed class OrderListState {
    object Loading : OrderListState()
    data class Success(val orders: List<Order>) : OrderListState()
    data class Error(val message: String?) : OrderListState()
}

Clean Architecture

Presentation Layer (UI + ViewModels)
  ↓ depends on
Domain Layer (Use Cases + Entities)
  ↓ depends on
Data Layer (Repositories + Data Sources)

Key rule: Dependencies point inward.
  - Domain knows nothing about Presentation or Data.
  - Data implements interfaces defined by Domain.

Use Case Pattern

class GetOrdersUseCase(
    private val orderRepository: OrderRepository,
    private val userRepository: UserRepository
) {
    suspend operator fun invoke(): Result<List<OrderWithUser>> {
        val user = userRepository.getCurrentUser() ?: return Result.failure(NotLoggedInError())
        val orders = orderRepository.getOrdersForUser(user.id)
        return Result.success(orders.map { OrderWithUser(it, user) })
    }
}

Repository Pattern

interface OrderRepository {
    suspend fun getOrders(): Result<List<Order>>
    suspend fun getOrder(id: String): Result<Order>
    suspend fun createOrder(request: CreateOrderRequest): Result<Order>
}

class OrderRepositoryImpl(
    private val api: OrderApi,
    private val db: OrderDao,
    private val cache: OrderCache
) : OrderRepository {
    
    override suspend fun getOrders(): Result<List<Order>> {
        // Cache-first strategy
        cache.getOrders()?.let { return Result.success(it) }
        
        return try {
            val orders = api.getOrders()
            db.insertAll(orders)
            cache.setOrders(orders)
            Result.success(orders)
        } catch (e: Exception) {
            // Fall back to local database
            val localOrders = db.getAllOrders()
            if (localOrders.isNotEmpty()) Result.success(localOrders)
            else Result.failure(e)
        }
    }
}

// Separate navigation logic from views
class OrderCoordinator: Coordinator {
    var navigationController: UINavigationController
    
    func start() {
        let viewModel = OrderListViewModel()
        viewModel.onOrderSelected = { [weak self] order in
            self?.showOrderDetail(order)
        }
        let view = OrderListView(viewModel: viewModel)
        navigationController.push(view)
    }
    
    private func showOrderDetail(_ order: Order) {
        let viewModel = OrderDetailViewModel(order: order)
        viewModel.onPaymentTapped = { [weak self] in
            self?.showPayment(for: order)
        }
        let view = OrderDetailView(viewModel: viewModel)
        navigationController.push(view)
    }
}

Anti-Patterns

Anti-PatternConsequenceFix
Massive ViewController/ActivityUntestable, merge conflictsExtract to ViewModel + Use Cases
Network calls in View layerCannot test UI logicRepository pattern, dependency injection
God ViewModel (1000+ lines)Same problem as fat controllersSplit into focused ViewModels
Skipping domain layerBusiness logic in ViewModelsUse Cases encapsulate business rules
Tight coupling to frameworksCannot unit testDepend on abstractions, inject dependencies

Mobile architecture is not about following a pattern perfectly — it is about creating boundaries that let you test, maintain, and evolve the codebase as the team and feature set grow.

Jakub Dimitri Rezayev
Jakub Dimitri Rezayev
Founder & Chief Architect • Garnet Grid Consulting

Jakub holds an M.S. in Customer Intelligence & Analytics and a B.S. in Finance & Computer Science from Pace University. With deep expertise spanning D365 F&O, Azure, Power BI, and AI/ML systems, he architects enterprise solutions that bridge legacy systems and modern technology — and has led multi-million dollar ERP implementations for Fortune 500 supply chains.

View Full Profile →