Clean Architecture — fundamentos, camadas de Entidades, Casos de Uso e Gateways

Autor: IT Sectr Publicado: 2026-02-17 Tempo de leitura: 10 min

Clean Architecture — uma arquitetura em camadas proposta por Robert Martin (Uncle Bob) em 2012, dividindo uma aplicação em camadas independentes: Domain (Entities, Use Cases), Data (Repositories, DataSources) e Presentation (ViewModels, Views). O princípio principal é a Dependency Rule: as dependências apontam para dentro, as camadas externas dependem das internas, mas não o contrário. A Clean Architecture é usada no desenvolvimento móvel para projetos com alta complexidade de lógica de negócios. Mais detalhes no livro The Clean Architecture.

Pontos Principais

  • Clean Architecture — três camadas: Domain (lógica de negócios), Data (dados), Presentation (UI) com Dependency Rule
  • Dependency Rule — dependências apontam para dentro, Domain não sabe sobre Data e Presentation
  • Use Cases (Interactors) — cenários de lógica de negócios, cada Use Case é uma classe com um método
  • Repository Interface — abstração de dados no Domain, implementação na camada Data
  • Capacidade de teste — Domain e Use Cases são testados com testes unitários sem Android SDK e iOS UIKit

Clean Architecture — fundamentos da arquitetura em camadas

Clean Architecture — um padrão arquitetônico formulado por Robert Martin (Uncle Bob) em 2012. A ideia central é dividir uma aplicação em camadas com uma regra estrita de dependências: o código dentro de uma camada não sabe nada sobre o código externo. As camadas externas (UI, frameworks, BD) são detalhes de implementação. As camadas internas (lógica de negócios, regras empresariais) são a essência da aplicação.

Camadas da Clean Architecture no desenvolvimento móvel: 1) Domain — Entities (objetos de negócio) e Use Cases (cenários de uso); 2) Data — RepositoryImpl (implementações de repositórios), DataSources (rede, BD, cache); 3) Presentation — ViewModels, Views (Compose/SwiftUI). Domain é a camada mais interna sem dependências. Data depende de Domain (implementa interfaces de repositório). Presentation depende de Domain (chama Use Cases, subscreve resultados).

CamadaContémDependências
DomainEntities, Use Cases, Repository InterfacesNenhuma (Kotlin/Swift puro)
DataRepositoryImpl, DataSources (API, BD, Cache)Domain, Retrofit, Room, Ktor
PresentationViewModels, Views, ComposablesDomain, Jetpack, SwiftUI

Dependency Rule — a única regra estrita da Clean Architecture. O código fonte só pode referenciar uma camada dentro de si mesma ou uma camada inferior (mais próxima do centro). Presentation importa Domain. Domain NÃO importa Data ou Presentation. Isto é alcançado através do Princípio de Inversão de Dependência: Domain define a interface Repository, Data a implementa. Presentation depende da abstração UseCase, não de um repositório específico.

Domain Layer: Entidades, Casos de Uso e Interfaces de Repositório

Domain — a camada mais estável da aplicação. Entities são objetos de negócio independentes de frameworks: User, Product, Order. Use Cases são classes com um único método invoke (ou operator fun invoke em Kotlin), implementando um cenário: GetUserUseCase, PlaceOrderUseCase, CalculateTotalUseCase. Repository Interfaces são abstrações de acesso a dados definidas no Domain, implementadas no Data. Domain não contém Android SDK, iOS UIKit, Retrofit, Room — apenas Kotlin ou Swift puro.

swift
// Entity — objeto de negócio (Domain)
struct User: Equatable {
    let id: Int
    let name: String
    let email: String
}

// Repository Interface — abstração de dados (Domain)
protocol UserRepository {
    func getUser(id: Int) async throws -> User
    func getUsers() async throws -> [User]
}

// Use Case — um cenário (Domain)
final class GetUserUseCase {
    private let repository: UserRepository

    init(repository: UserRepository) {
        self.repository = repository
    }

    func execute(id: Int) async throws -> User {
        return try await repository.getUser(id: id)
    }
}

Use Case — «uma classe com um método» não é um dogma mas uma recomendação prática. Quando um Use Case se torna mais complexo (validação + registro + chamada de repositório), seus métodos são agrupados por significado: UserUseCase.getUser, UserUseCase.searchUsers, UserUseCase.deleteUser. O principal é que um Use Case não deve saber de onde vêm os dados (rede, BD, cache) nem quem os exibe (Compose, SwiftUI). Na IT Sectr alocamos um Use Case para cada operação que tenha uma regra de negócio, validação ou combinação de dados de duas fontes.

Pureza do Domain é alcançada através do mapeamento DTO nos limites das camadas. A camada Data recebe modelos JSON (DTO), mapeia-os para Entities do Domain. Presentation recebe Entities do Domain, mapeia-os para ViewModels (DisplayItem). Uma Entity do Domain nunca contém anotações Retrofit, Room ou Codable — isto garante que a camada não precisará ser alterada ao mudar de BD de Room para Realm ou ao substituir Retrofit por Ktor.

Data Layer: Implementação de Repositório e DataSources

Data Layer — implementação das interfaces definidas no Domain. Contém RepositoryImpl (classes que implementam UserRepository) e DataSources (RemoteDataSource — API, LocalDataSource — BD, CacheDataSource — SharedPreferences/NSUserDefaults). A camada Data depende do Domain (importa interfaces de repositório e Entities) e de frameworks (Retrofit, Room, Ktor, CoreData). RepositoryImpl oculta a fonte de dados do Domain — o Use Case não sabe se os dados vieram da rede ou do cache.

kotlin
// DTO — modelo para rede (Data)
data class UserDto(
    @SerializedName("id") val id: Int,
    @SerializedName("first_name") val firstName: String,
    @SerializedName("last_name") val lastName: String,
    @SerializedName("email") val email: String
)

// RepositoryImpl — implementação (Data)
class UserRepositoryImpl(
    private val remoteDataSource: UserRemoteDataSource,
    private val localDataSource: UserLocalDataSource
) : UserRepository {

    override suspend fun getUser(id: Int): User {
        // A tentar obter do cache
        localDataSource.getUser(id)?.let { return it.toDomain() }
        // Se não — carregar da rede
        val dto = remoteDataSource.fetchUser(id)
        val user = dto.toDomain()
        localDataSource.saveUser(user)
        return user
    }

    override suspend fun getUsers(): List<User> {
        return remoteDataSource.fetchAllUsers().map { it.toDomain() }
    }
}

// Mapper — conversão DTO ↔ Domain
fun UserDto.toDomain() = User(
    id = id,
    name = "$firstName $lastName",
    email = email
)

Estratégia de Cache na Data Layer: RepositoryImpl primeiro verifica o armazenamento local; se não encontrar dados, carrega da rede e salva localmente. Se a rede estiver indisponível, retorna dados obsoletos com uma flag isStale. O Use Case no Domain não sabe da estratégia — recebe User através de Repository.getUser(id). Alterar a estratégia (por exemplo, invalidação de cache a cada 15 minutos) não afeta Domain nem Presentation.

Modularidade no Android — Kotlin Multiplatform permite extrair o Domain para um módulo KMP separado sem dependências do Android SDK. Data é um módulo separado com dependência de Domain. Presentation é um módulo Android com dependência de Domain. Dependências Gradle: domain (Kotlin puro), data (domain + Retrofit + Room), app (domain + presentation + Hilt). Tal modularidade é essencial para grandes projetos — CI compila Domain separadamente, os testes unitários do Domain não requerem um emulador Android.

Presentation Layer: ViewModels e Views

Presentation Layer — a camada mais externa da Clean Architecture. Contém ViewModels (Android) / ObservableObject (iOS) e Views (Compose/SwiftUI). O ViewModel chama um Use Case, recebe o resultado e transforma em estado de UI. A View subscreve o Estado e renderiza. Presentation depende do Domain — importa Use Cases e Entities. Presentation não importa a camada Data — os dados vêm através do Use Case, que internamente usa um Repository.

ViewModel na Clean Architecture não contém lógica de negócio — chama o Use Case. Se um Use Case retorna User, o ViewModel transforma-o em UserDisplayItem (name, emailFormatted, avatarUrl) — um modelo puramente de apresentação. O Use Case não sabe sobre DisplayItem — retorna uma Entity. Esta separação permite testar o Use Case sem a UI e o ViewModel sem o UseCase (através de mocks). Na IT Sectr seguimos estritamente: Use Case — lógica de negócio, ViewModel — apenas apresentação, View — apenas exibição.

kotlin
// Use Case (Domain) — lógica de negócios pura
class GetUserUseCase(
    private val repository: UserRepository
) {
    suspend operator fun invoke(id: Int): User {
        return repository.getUser(id)
    }
}

// ViewModel (Presentation) — apenas apresentação
class UserViewModel(
    private val getUserUseCase: GetUserUseCase
) : ViewModel() {

    private val _state = MutableStateFlow<UserScreenState>(UserScreenState.Loading)
    val state: StateFlow<UserScreenState> = _state.asStateFlow()

    fun loadUser(id: Int) {
        viewModelScope.launch {
            _state.value = UserScreenState.Loading
            val user = getUserUseCase(id)
            val displayItem = UserDisplayItem(
                name = user.name,
                email = user.email,
                initials = user.name.split(" ").joinToString("") { it.first().toString() }
            )
            _state.value = UserScreenState.Success(displayItem)
        }
    }
}

data class UserDisplayItem(
    val name: String,
    val email: String,
    val initials: String
)

sealed interface UserScreenState {
    data object Loading : UserScreenState
    data class Success(val displayItem: UserDisplayItem) : UserScreenState
    data class Error(val message: String) : UserScreenState
}

Navegação na camada Presentation também faz parte do anel externo. Clean Architecture não prescreve um mecanismo de navegação — pode ser NavController (Compose), NavigationStack (SwiftUI), Coordinator (UIKit) ou Router (VIPER). É importante que as decisões de navegação são tomadas pela Presentation, mas a navegação não deve penetrar no Use Case. O Use Case retorna um resultado, o ViewModel decide para qual ecrã navegar. Na Clean Architecture, a navegação é um detalhe que pode ser substituído sem alterar o Domain.

Clean Architecture no iOS e Android: exemplos de código

Clean Architecture no Android é implementada através de módulos Gradle: domain (Kotlin puro), data (domain + Retrofit + Room), presentation (domain + Compose). Estrutura de pastas: domain/user/User.kt, GetUserUseCase.kt, UserRepository.kt; data/remote/UserRemoteDataSource.kt, local/UserDao.kt, repository/UserRepositoryImpl.kt; presentation/ui/user/UserViewModel.kt, UserScreen.kt. DI (Hilt) conecta as camadas: UserRepositoryImpl é vinculado à interface UserRepository no módulo domain.

Clean Architecture no iOS usa SPM ou grupos Xcode sem módulos separados (devido a limitações do Xcode). Domain — uma pasta com ficheiros que não importam UIKit ou SwiftUI. Data — uma pasta com APIClient, CoreDataStack, RepositoryImpl. Presentation — uma pasta com ViewModels e SwiftUI Views. DI através de construtor ou montagem na App. A chamada principal é async/await através de UseCase.execute() com verificação de MainActor para atualizações de UI.

swift
// Data Layer: Remote DataSource (iOS)
final class UserRemoteDataSource {
    private let apiClient: APIClient

    func fetchUser(id: Int) async throws -> UserDTO {
        return try await apiClient.get("/users/\(id)")
    }
}

// Repository Implementation (Data)
final class UserRepositoryImpl: UserRepository {
    private let remote: UserRemoteDataSource
    private let local: UserLocalDataSource

    func getUser(id: Int) async throws -> User {
        if let cached = try await local.getUser(id) {
            return cached
        }
        let dto = try await remote.fetchUser(id)
        let user = dto.toDomain()
        try await local.saveUser(user)
        return user
    }
}

// Presentation: ViewModel + SwiftUI View
@MainActor
final class UserViewModel: ObservableObject {
    @Published private(set) var state: UserScreenState = .loading
    private let getUserUseCase: GetUserUseCase

    func loadUser(id: Int) {
        Task {
            state = .loading
            if let user = try? await getUserUseCase.execute(id: id) {
                state = .success(user)
            } else {
                state = .error("Failed to load")
            }
        }
    }
}

Clean Architecture em projetos IT Sectr — nosso padrão para projetos a partir de 30 dias. Estamos usando uma arquitetura de três camadas com Kotlin Multiplatform para Android/iOS desde 2022. Domain — um módulo KMP partilhado, Data — módulos de plataforma (Retrofit no Android, URLSession no iOS), Presentation — UI nativa. Isto fornece 60–80% de código de lógica de negócios partilhado entre iOS e Android, reduzindo o tempo de desenvolvimento em 30–40% comparado a duas implementações separadas.

Perguntas Frequentes

Quantas camadas deve ter a Clean Architecture?

No mínimo três: Domain, Data, Presentation. Para grandes projetos, adicionam-se Framework (dependências Android SDK/iOS UIKit) e Device (GPS, câmara, sensores). O número de camadas não é uma regra estrita mas uma questão de conveniência. O principal é seguir a Dependency Rule: as dependências apontam para dentro, em direção ao Domain. Pode começar com três e adicionar camadas à medida que o projeto cresce.

A Clean Architecture aumenta a quantidade de código?

Sim — em 30–50% comparado ao MVVM devido à extração de interfaces de repositório, Use Cases e mappers. Isto é excessivo para uma aplicação CRUD simples. Clean Architecture justifica-se para projetos com lógica de negócios complexa onde a capacidade de teste e o isolamento de camadas são mais importantes que a velocidade de desenvolvimento. Para MVP ou protótipos, use MVVM — Clean Architecture atrasará o início.

Pode combinar Clean Architecture com MVI?

Sim, é uma prática comum. Os Use Cases permanecem no Domain, enquanto a Presentation usa o ciclo MVI (Intent → Reducer → State). A camada Data permanece a mesma, o Domain permanece o mesmo. MVI na Presentation fornece estado de ecrã previsível, Clean Architecture fornece isolamento da lógica de negócios. Esta combinação é usada em grandes projetos com dezenas de desenvolvedores.

Preciso de Use Cases para cada pedido de dados?

Um Use Case é necessário quando a operação envolve uma regra de negócio: validação, combinação de dados de duas fontes, cálculo, registro, verificação de permissões de acesso. Um simples pedido getUser(id) sem lógica adicional pode chamar o Repository diretamente do ViewModel. No entanto, para consistência arquitetónica, muitas equipas criam um Use Case para cada método público do Repository — isto adiciona 5–10% de código mas simplifica a leitura.

Como testar a Clean Architecture?

Domain: Testes unitários de Use Cases com Repository mock — Kotlin/Swift puro sem Android SDK. Data: Testes de integração de RepositoryImpl com DataSource mock/fake. Presentation: Testes de ViewModel com UseCase mock. Graças à Dependency Rule, cada camada é testada isoladamente. Na IT Sectr, a cobertura do Domain atinge 95%, Data — 70–80%, Presentation — 60–70%.

Resumo

  • Clean Architecture — três camadas (Domain, Data, Presentation) com Dependency Rule apontando para dentro
  • Dependency Rule — Domain não sabe sobre Data e Presentation, isolamento através de interfaces
  • Domain — Entities, Use Cases, Repository Interfaces — Kotlin/Swift puro sem frameworks
  • Data — RepositoryImpl, DataSources (rede, BD, cache) — implementação de interfaces do Domain
  • Presentation — ViewModels, Views — apenas exibição, lógica de negócios nos Use Cases
  • Testes — Domain coberto por testes unitários em 90–95%
  • KMP — Clean Architecture com Kotlin Multiplatform fornece 60–80% de código partilhado iOS + Android

Vamos desenvolver um aplicativo móvel chave na mão

A IT Sectr cria aplicativos para iOS e Android para startups e empresas desde 2017. Nós vamos aconselhá-lo e propor a melhor solução.

Discutir o projeto

Leia também