Mobile

23 mar, 2026

Arquitetura Mobile em escala para um sistema sustentável

Publicidade

Mobile não quebra quando você tem 1.000 usuários. Ele quebra quando você tem 1 milhão.

E não é por causa da UI. É por causa da arquitetura.

Se você ainda organiza seu app por “telas” ou “pastas de features soltas”, cedo ou tarde vai enfrentar:

  • código impossível de manter
  • bugs que ninguém sabe de onde vêm
  • deploys com medo
  • refactors que nunca acontecem

Sênior não constrói tela. Sênior constrói sistema evolutivo.

O problema: apps mobile viram monólitos silenciosos

No começo, tudo parece simples:

/screens
  HomeScreen.tsx
  ProfileScreen.tsx
/services
  api.ts
/utils
  helpers.ts

Aí o app cresce.

E de repente:

  • api.ts tem 2.000 linhas
  • helpers.ts virou um Frankenstein
  • lógica de negócio tá misturada com UI
  • ninguém sabe o impacto de uma mudança

Você criou um monólito.

Só que pior: um monólito sem disciplina de backend.

Clean Architecture no mobile (de verdade, não de slide)

A ideia é simples:

Separar o que o app faz de como ele faz.

Estrutura base:

/domain
  /entities
  /usecases

/data
  /repositories
  /datasources

/presentation
  /ui
  /viewmodels

Agora vamos ver isso na prática.

Exemplo: caso de uso

class GetUserProfileUseCase(
    private val repository: UserRepository
) {
    suspend operator fun invoke(userId: String): User {
        return repository.getUser(userId)
    }
}

Aqui não tem Retrofit, não tem JSON, não tem UI.

Só regra de negócio.

Repository (camada de abstração)

interface UserRepository {
    suspend fun getUser(userId: String): User
}

Implementação (data layer)

class UserRepositoryImpl(
    private val api: UserApi
) : UserRepository {

    override suspend fun getUser(userId: String): User {
        val response = api.getUser(userId)
        return response.toDomain()
    }
}

UI consumindo isso

class ProfileViewModel(
    private val getUserProfile: GetUserProfileUseCase
) : ViewModel() {

    val state = MutableStateFlow<User?>(null)

    fun load(userId: String) {
        viewModelScope.launch {
            state.value = getUserProfile(userId)
        }
    }
}

👉 Resultado:

  • UI desacoplada
  • regra de negócio testável
  • troca de API sem quebrar tudo

Modularização: quando um app vira um ecossistema

Quando seu app cresce, um único módulo vira gargalo.

A solução: feature modules.

/features
  /home
  /profile
  /checkout

/core
  /network
  /designsystem
  /utils

Cada feature tem:

  • UI
  • ViewModel
  • UseCases
  • Repositórios próprios

Exemplo de estrutura de feature

/features/profile
  ProfileScreen.kt
  ProfileViewModel.kt
  GetUserProfileUseCase.kt
  ProfileRepository.kt

👉 Benefícios reais:

  • times trabalham em paralelo
  • builds mais rápidos
  • isolamento de bugs
  • deploy mais previsível

Gerenciamento de estado: o caos invisível

Sem estratégia de estado, seu app vira isso:

  • loading duplicado
  • dados inconsistentes
  • telas com comportamentos estranhos

Exemplo com Redux (React Native)

const initialState = {
  user: null,
  loading: false,
  error: null
}

function reducer(state = initialState, action) {
  switch (action.type) {
    case 'FETCH_USER':
      return { ...state, loading: true }
    case 'FETCH_USER_SUCCESS':
      return { ...state, loading: false, user: action.payload }
    case 'FETCH_USER_ERROR':
      return { ...state, loading: false, error: action.error }
    default:
      return state
  }
}

Dispatch com async

export const fetchUser = (id) => async (dispatch) => {
  dispatch({ type: 'FETCH_USER' })

  try {
    const data = await api.getUser(id)
    dispatch({ type: 'FETCH_USER_SUCCESS', payload: data })
  } catch (e) {
    dispatch({ type: 'FETCH_USER_ERROR', error: e })
  }
}

👉 Sênior não escolhe ferramenta.

Escolhe previsibilidade de estado.

Offline-first: o mundo real não tem Wi-Fi perfeito

Se seu app depende 100% da API, ele já nasceu frágil.

Estratégia: cache + sincronização.

Exemplo simples (Kotlin + Room)

suspend fun getUser(userId: String): User {
    val cached = database.userDao().getUser(userId)

    return try {
        val remote = api.getUser(userId)
        database.userDao().insert(remote.toEntity())
        remote.toDomain()
    } catch (e: Exception) {
        cached.toDomain()
    }
}

👉 Isso aqui muda o jogo:

  • app funciona offline
  • UX muito melhor
  • menos dependência de backend

🔁 Sincronização inteligente

Offline-first não é só cache.

É resolver conflitos.

Exemplo de sync (pseudo-código)

fun syncData(local: List<Item>, remote: List<Item>): List<Item> {
    return resolveConflicts(local, remote)
}

Estratégias reais:

  • last write wins
  • versionamento
  • merge de dados

Monorepo vs Multirepo (decisão de time, não de código)

Monorepo

  • tudo junto
  • fácil compartilhar código
  • builds mais pesados

Multirepo

  • isolamento total
  • mais controle
  • overhead de integração

👉 Sênior pensa assim:

“Isso aqui escala com o time ou só com o código?”

O ponto que separa sênior de pleno

Pleno resolve feature.

Sênior resolve complexidade ao longo do tempo.

Arquitetura mobile em escala não é sobre:

  • usar Clean Architecture porque tá na moda
  • dividir pasta bonitinha
  • escolher Redux ou Bloc

É sobre:

  • reduzir acoplamento
  • permitir evolução contínua
  • evitar reescrita total em 1 ano

Conclusão

Se seu app:

  • cresce rápido
  • tem múltiplos devs
  • depende de backend
  • precisa evoluir constantemente

Então arquitetura não é luxo.

É sobrevivência.