Arquitetura

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

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.