Clean Architecture
Organiza o sistema em camadas concêntricas onde as dependências só apontam para dentro — frameworks e bancos de dados ficam na borda externa, entidades de negócio no núcleo — e boundaries explícitos isolam cada camada por meio de inversão de dependência.
Intenção
Tornar os detalhes de implementação — frameworks, banco de dados, UI, dispositivos — substituíveis sem afetar a lógica de negócio. A Clean Architecture, descrita por Robert C. Martin em 2012, formula a regra da dependência: o código-fonte de uma camada interna nunca pode mencionar nada de uma camada externa. As dependências só apontam para dentro.
O ponto central não é o diagrama de círculos em si, mas a regra que ele representa: entidades de negócio não conhecem casos de uso; casos de uso não conhecem controllers; controllers não conhecem o banco de dados. Cada cruzamento de fronteira usa uma abstração — uma interface ou um DTO — de modo que a camada interna define o contrato e a externa o implementa.
A Clean Architecture é convergente com a Arquitetura Hexagonal e com a Onion Architecture: todas compartilham a ideia de domínio isolado no centro e infraestrutura nas bordas. A diferença está na granularidade: a Clean Architecture detalha explicitamente a separação entre entidades (regras de negócio puras) e casos de uso (regras de aplicação), e torna os boundaries (interfaces de cruzamento de fronteira) um elemento formal do modelo.
Problema
O problema que a Clean Architecture ataca é o acoplamento ao detalhe:
- Acoplamento ao framework: quando as entidades herdam de classes do framework (ActiveRecord, JPA Entity), mudar de ORM requer reescrever o domínio. O framework deveria ser um detalhe substituível, não a base da arquitetura.
- Acoplamento ao banco de dados: regras de negócio que dependem de SQL, de esquemas específicos ou de transações de banco tornam o domínio intestável sem infraestrutura real.
- Acoplamento à UI: lógica de negócio espalhada em controllers, componentes de UI ou handlers HTTP que precisam ser replicados ao adicionar um novo canal de entrada (CLI, fila, API GraphQL).
- Testabilidade comprometida: quando uma regra de negócio simples ("o desconto máximo é 30%") só pode ser verificada subindo um servidor HTTP com banco de dados, o custo de testar é alto e o ciclo de feedback é lento.
Estrutura
A Clean Architecture é representada como círculos concêntricos. Cada círculo externo é um detalhe; cada círculo interno é uma política. A regra é simples: nunca um círculo interno pode depender de um círculo externo.
╔═══════════════════════════════════════════════════════════════════╗
║ FRAMEWORKS & DRIVERS (borda mais externa) ║
║ Web, banco de dados, UI, dispositivos, serviços externos ║
║ ┌─────────────────────────────────────────────────────────────┐ ║
║ │ ADAPTADORES DE INTERFACE │ ║
║ │ Controllers, Presenters, Gateways, Serializers │ ║
║ │ ┌───────────────────────────────────────────────────────┐ │ ║
║ │ │ CASOS DE USO (Application Business Rules) │ │ ║
║ │ │ Use Case Interactors, Application Services │ │ ║
║ │ │ ┌─────────────────────────────────────────────────┐ │ │ ║
║ │ │ │ ENTIDADES (Enterprise Business Rules) │ │ │ ║
║ │ │ │ Entidades, Value Objects, Regras de Negócio │ │ │ ║
║ │ │ │ Puras — sem dependência de nada externo │ │ │ ║
║ │ │ └─────────────────────────────────────────────────┘ │ │ ║
║ │ └───────────────────────────────────────────────────────┘ │ ║
║ └─────────────────────────────────────────────────────────────┘ ║
╚═══════════════════════════════════════════════════════════════════╝
Regra da dependência: as setas apontam APENAS para dentro.
Entidades não conhecem Casos de Uso.
Casos de Uso não conhecem Controllers.
Controllers não conhecem Frameworks diretamente (usam interfaces).
As quatro camadas
- Entidades: encapsulam as regras de negócio mais gerais e de alto nível da empresa. São os objetos que existiriam mesmo sem nenhum software — as políticas que valem para qualquer sistema que implemente esse domínio. Não dependem de nada externo.
- Casos de uso: contêm as regras de negócio específicas da aplicação. Orquestram o fluxo de dados de e para as entidades, e direcionam as entidades a usarem suas regras de negócio para atingir o objetivo do caso de uso. Conhecem entidades, mas não conhecem controllers nem banco de dados.
- Adaptadores de interface: convertem dados entre o formato mais conveniente para os casos de uso e entidades e o formato mais conveniente para algum agente externo como banco de dados ou web. Controllers, Presenters e Gateways vivem aqui.
- Frameworks e drivers: a camada mais externa. Aqui ficam os detalhes: o framework web, o banco de dados, serviços de e-mail. Você não deve escrever muito código aqui — apenas código de ligação que comunica para dentro com a próxima camada.
Boundaries e DTOs
Quando os dados cruzam a fronteira entre camadas, eles devem fazê-lo por meio de estruturas simples de dados — DTOs (Data Transfer Objects) — e nunca por meio de entidades da camada interna. Isso garante que a camada externa não arraste dependências para dentro.
O cruzamento acontece por meio de um boundary: uma interface definida pela camada interna que a camada externa implementa. O caso de uso define a interface do repositório que precisa; a infraestrutura implementa essa interface. O caso de uso nunca importa a implementação concreta.
Como funciona — a regra da dependência na prática
O trecho abaixo mostra a regra da dependência aplicada a um caso de uso simples. A interface do repositório é definida dentro da camada de casos de uso; a implementação concreta fica na infraestrutura e aponta para dentro — nunca o contrário.
// ── ENTIDADE (camada mais interna) ───────────────────────────
// Sem imports externos. Só regras de negócio puras.
class Pedido {
constructor(
readonly id: string,
readonly itens: ItemPedido[],
) {}
calcularTotal(): number {
return this.itens.reduce((acc, item) => acc + item.subtotal(), 0);
}
validar(): void {
if (this.itens.length === 0) {
throw new Error('Pedido deve ter ao menos um item');
}
}
}
// ── CASO DE USO (conhece Entidade; define sua própria interface) ──
// OutputBoundary é definido aqui — a camada externa o implementa.
interface CriarPedidoOutputBoundary {
apresentar(resultado: { pedidoId: string; total: number }): void;
}
interface PedidoRepository {
salvar(pedido: Pedido): Promise<void>;
}
class CriarPedidoUseCase {
constructor(
private readonly repo: PedidoRepository,
private readonly output: CriarPedidoOutputBoundary,
) {}
async executar(dto: { itens: { produtoId: string; quantidade: number; preco: number }[] }): Promise<void> {
const pedido = new Pedido(
crypto.randomUUID(),
dto.itens.map(i => new ItemPedido(i.produtoId, i.quantidade, i.preco)),
);
pedido.validar();
await this.repo.salvar(pedido);
this.output.apresentar({ pedidoId: pedido.id, total: pedido.calcularTotal() });
}
}
// ── ADAPTADOR DE INTERFACE (Controller) ──────────────────────
// Conhece o caso de uso; não conhece Entidade diretamente.
class CriarPedidoController {
constructor(private readonly useCase: CriarPedidoUseCase) {}
async handle(req: Request): Promise<void> {
await this.useCase.executar({ itens: req.body.itens });
}
}
// ── INFRAESTRUTURA (borda externa) implementa interface interna ──
// A dependência aponta para DENTRO: Infra conhece o Caso de Uso.
class PostgresPedidoRepository implements PedidoRepository {
async salvar(pedido: Pedido): Promise<void> {
// acessa o banco; o Caso de Uso não sabe desse detalhe
}
}
O ponto crítico: PedidoRepository e
CriarPedidoOutputBoundary são interfaces declaradas dentro
da camada de casos de uso. PostgresPedidoRepository fica na
infraestrutura e importa o caso de uso — a dependência aponta para dentro.
O caso de uso nunca importa PostgresPedidoRepository.
Quando usar
- Sistemas com domínio de negócio rico e longevidade alta: quanto mais complexas as regras de negócio e maior o ciclo de vida do sistema, mais o investimento nos boundaries se paga. A capacidade de substituir o banco de dados, o framework ou o canal de entrada sem tocar no domínio tem valor crescente com o tempo.
- Quando a testabilidade é prioridade: a separação explícita de entidades e casos de uso permite testar toda a lógica de negócio com implementações in-memory dos repositórios — sem banco, sem HTTP, com execução em milissegundos.
- Múltiplos canais de entrada: o mesmo conjunto de casos de uso pode ser acionado por HTTP, CLI, mensageria ou testes sem modificação, pois cada canal é apenas um adaptador de interface diferente.
- Equipes que precisam de fronteiras claras: a separação formal entre camadas — reforçada por boundaries — permite que equipes diferentes trabalhem em camadas diferentes com acoplamento mínimo.
Quando evitar
- CRUDs simples sem lógica de domínio: criar entidades, casos de uso, boundaries e DTOs para uma aplicação que apenas persiste e lê registros sem regras complexas é over-engineering com custo real. Uma Arquitetura em Camadas simples ou até MVC direto é mais produtivo.
- Projetos de vida curta ou equipes pequenas sem experiência: a Clean Architecture exige que a equipe entenda inversão de dependência, DTOs de boundary e a distinção entre entidade e caso de uso. Sem esse entendimento, o resultado costuma ser uma Arquitetura em Camadas com nomes diferentes e mappers redundantes sem propósito claro.
- Quando os mappers de boundary se tornam o maior volume de código: um sinal de que a arquitetura pode estar sendo aplicada com granularidade excessiva — mapear 1:1 cada campo entre camadas adjacentes sem transformação real não agrega valor.
Prós e contras
Prós
- Independência de framework: o domínio não depende de nenhuma biblioteca externa — frameworks são detalhes substituíveis.
- Testabilidade total do domínio sem infraestrutura real: entidades e casos de uso são testados com mocks simples das interfaces.
- Independência de banco de dados: trocar de PostgreSQL para MongoDB (ou para in-memory) é trocar o adaptador de repositório, sem tocar no domínio.
- Independência de UI: a mesma lógica de negócio serve HTTP, CLI, testes e filas — cada um com seu próprio adaptador.
- Fronteiras formais (boundaries) explicitam os contratos entre camadas — servem como documentação viva da arquitetura.
Contras
- Volume de código maior: interfaces de boundary, DTOs de entrada/saída e mappers entre camadas multiplicam o número de arquivos mesmo em funcionalidades simples.
- Curva de aprendizado: a distinção entre entidade, caso de uso, adaptador e framework — e a inversão de dependência que atravessa tudo — não é intuitiva para quem vê pela primeira vez.
- Risco de over-engineering para domínios simples: aplicar quatro camadas concêntricas com boundaries formais em um CRUD básico gera custo sem benefício equivalente.
- Mappers redundantes: com granularidade excessiva, os conversores de DTO entre camadas adjacentes se tornam o maior volume de código do projeto sem agregar lógica de negócio.
Armadilhas comuns
1. Mapear 1:1 pastas às quatro camadas e criar mappers redundantes
A armadilha mais comum é criar quatro diretórios (entities/,
use-cases/, adapters/, frameworks/)
e converter mecanicamente cada objeto entre camadas, mesmo quando não há
transformação real. Se o DTO de entrada do caso de uso é estruturalmente
idêntico ao payload da requisição HTTP, um mapper que copia campo por campo
sem transformação não agrega valor — agrega apenas volume e complexidade de
manutenção.
A regra da dependência é o que importa, não a quantidade de diretórios. A estrutura de pastas deve refletir as fronteiras reais de dependência, não imitar o diagrama de círculos de forma literal.
2. Inverter a dependência no sentido errado
O erro mais grave e mais silencioso: o caso de uso importa a implementação
concreta do repositório em vez de importar a interface. Isso acontece quando
a interface PedidoRepository é declarada no pacote de
infraestrutura e o caso de uso importa esse pacote. A dependência deveria
ser o contrário: a interface vive dentro do pacote de casos de uso, e a
infraestrutura importa o caso de uso para implementar a interface.
Teste rápido: se você deletar a pasta de infraestrutura, o código de entidades e casos de uso ainda deve compilar sem erros. Se não compilar, há dependência invertida no sentido errado em algum ponto.
3. Over-engineering para CRUD
Aplicar Clean Architecture a um serviço que cria, lê, atualiza e deleta registros sem regras de negócio complexas gera um custo real de desenvolvimento e manutenção sem benefício equivalente. O princípio "o detalhe não deve influenciar a política" só tem valor quando existe política (regra de negócio) a proteger. Para CRUDs, considere uma abordagem mais simples e refatore conforme a complexidade do domínio crescer.
4. Ignorar a camada de entidades e colocar tudo nos casos de uso
Quando todas as regras de negócio ficam nos interactors de caso de uso e as entidades são apenas estruturas de dados sem comportamento, o resultado é um domínio anêmico — o mesmo problema da Arquitetura em Camadas mal aplicada. As entidades devem encapsular as regras mais gerais e estáveis do negócio; os casos de uso orquestram essas entidades para atingir objetivos específicos da aplicação.
Arquiteturas e padrões relacionados
- Arquitetura Hexagonal (Ports & Adapters)
- Onion Architecture
- Arquitetura em Camadas
- MVC
- Adapter (GoF)
A Arquitetura Hexagonal é a arquitetura mais próxima da Clean Architecture e ambas são frequentemente confundidas. A diferença central está na granularidade: a Hexagonal fala em portas (interfaces) e adaptadores (implementações), sem distinguir formalmente entre entidades e casos de uso dentro do núcleo. A Clean Architecture adiciona essa distinção explícita e formaliza os boundaries como elemento da arquitetura. Na prática, as duas são compatíveis e convergentes — uma implementação hexagonal bem estruturada tipicamente respeita a regra da dependência da Clean Architecture.
A Onion Architecture (Jeffrey Palermo, 2008) é a precursora mais direta da Clean Architecture. Compartilha a ideia de domínio no centro, dependências apontando para dentro e infraestrutura na casca externa. A diferença principal é que a Onion enfatiza o domain model rico no núcleo (entidades com comportamento, serviços de domínio) e tende a ser menos prescritiva sobre a separação entre entidades e casos de uso do que a Clean Architecture.
A Arquitetura em Camadas é a forma mais simples de separar responsabilidades horizontalmente. A Clean Architecture a refina ao inverter explicitamente a dependência na fronteira entre domínio e infraestrutura — em vez de a infraestrutura ficar "abaixo" como dependência natural, ela fica "fora" e implementa interfaces definidas pelo domínio.
O padrão Adapter (GoF) é o mecanismo de implementação dos adaptadores de interface: cada gateway, controller ou presenter é um Adapter que converte entre o formato da camada interna e o formato do agente externo.