Logo NascimentoIgor Nascimento / Blog
Arquitetura

Clean Architecture: onde ela paga a conta e onde só adiciona pasta

Camadas não são o objetivo. O objetivo é conseguir trocar uma decisão sem reescrever o sistema. Uma conversa sobre quando esse seguro vale o prêmio, e quando você só está pagando por burocracia.

ArquiteturaBackend9 min de leituraIgor Nascimento

Começando pela pergunta errada

A pergunta que mais ouço sobre Clean Architecture é "quantas camadas eu preciso?". É a pergunta errada, e ela explica por que tanto projeto nasce com sete pastas vazias e morre com regra de negócio dentro do controller. Camada não é objetivo. Camada é um mecanismo para uma coisa só: conseguir trocar uma decisão sem reescrever o resto. Banco, framework web, fila, provedor de e-mail, a exchange que você consulta. Se nenhuma dessas decisões tem chance real de mudar, o mecanismo não paga o próprio custo.

Então a conversa que eu gosto de ter antes de abrir o editor é outra: quais decisões deste sistema eu quero poder reverter daqui a um ano? A resposta define onde vale colocar uma fronteira e onde um módulo simples resolve.

O que estou de fato comprando

Quando separo domínio, aplicação e infraestrutura, estou comprando três coisas concretas. Primeiro, testes que não precisam de banco nem de rede: o caso de uso recebe interfaces, então eu testo a regra com objetos em memória e deixo a integração para poucos testes mais lentos. Segundo, um vocabulário compartilhado: quando entidade, caso de uso e porta têm nome do negócio, a conversa com produto e dados fica mais curta. Terceiro, a possibilidade de trocar a borda: migrei um serviço de API REST para funções Lambda atrás de SQS sem tocar em nenhum caso de uso, porque a borda era só um adaptador.

E estou pagando com três coisas igualmente concretas. Mais arquivos por funcionalidade, o que torna o onboarding mais lento. Mapeamento entre DTOs, entidades e modelos de persistência, que é código chato e que erra em silêncio. E indireção: para achar onde uma query roda de verdade, o leitor atravessa uma interface e uma injeção de dependência.

Trade-off

Se o sistema é um CRUD com meia dúzia de telas e um time pequeno, o custo de indireção supera o ganho. Nesses casos prefiro módulos por funcionalidade, com a regra de negócio isolada em funções puras, e deixo a arquitetura em camadas para quando aparecer a segunda integração externa ou o segundo canal de entrada. Arquitetura boa é a que você consegue adiar.

O método que uso para descobrir as fronteiras

Quando o sistema justifica a estrutura, o problema vira outro: errar os nomes e os limites no início. Esse erro é o que dói mais tarde, porque renomear um domínio depois que ele virou tabela, fila e contrato de API é caro. O que faço é um exercício curto, antes de qualquer código, e é dele que saíram os checklists que eu publicava aqui antes.

  1. Listo os fluxos principais em frases curtas, sem pensar em tela. "Usuário cadastra operação", "sistema simula estratégia com histórico", "robô executa ordem real".
  2. Grifo os substantivos e os atores. Usuário, conta, operação, ordem, estratégia, histórico de preço, exchange. Cada substantivo que aparece em mais de um fluxo é candidato a entidade.
  3. Agrupo em domínios que não se misturam. Aqui está o erro mais comum: operation, order e market_data parecem a mesma coisa e não são. Uma operação é a intenção do usuário, uma ordem é o que foi mandado para a exchange, dado de mercado é o que veio de fora. Misturar os três gera uma tabela com trinta colunas opcionais.
  4. Modelo só as entidades principais, com nome e campos, sem pensar em ORM.
  5. Transformo cada ação em um caso de uso com nome de verbo: SimulateGridStrategy, CreateOrder, GetAccountBalance.
  6. Marco as fronteiras externas: API da exchange, banco, fila, storage. Cada uma vira uma interface que o domínio define e a infraestrutura implementa.

Só depois disso a estrutura de pastas aparece, e ela aparece quase sozinha:

src/ domain/ # entidades e regras puras, por domínio operation/ order/ market_data/ application/ # casos de uso, orquestração operation/ commands/ queries/ dtos/ infrastructure/ # implementações: banco, exchange, fila api/ # borda HTTP: routers e schemas shared/ # logging, utilitários

Onde a teoria quebra na prática

Transações que cruzam domínios

O diagrama bonito tem cada domínio isolado. A vida real tem "criar a ordem e debitar o saldo" na mesma transação. Dá para resolver com um unit of work que o caso de uso abre e os repositórios compartilham, mas isso é uma decisão arquitetural que o livro não toma por você. Minha regra: se duas entidades precisam ser consistentes na mesma transação com frequência, talvez elas sejam do mesmo domínio e eu separei errado.

O domínio anêmico

É fácil acabar com entidades que são só dataclasses e casos de uso gigantes que fazem tudo. Isso é a arquitetura em camadas com nome novo. O sinal de alerta é um caso de uso com mais de uma regra de negócio dentro de if. A regra pertence à entidade; o caso de uso só orquestra.

Performance de leitura

Repositório que devolve entidade completa é ótimo para escrita e péssimo para uma tela de listagem com paginação e filtros. Separo leitura de escrita quando isso aparece: a query fala com o banco diretamente e devolve um DTO, sem passar pela entidade. É uma concessão deliberada, documentada no nome da pasta (queries/), e não uma violação escondida.

O que eu faria diferente

Nos primeiros projetos em que apliquei isso, criei interface para tudo, inclusive para coisas que nunca teriam segunda implementação. Hoje só crio a porta quando existe uma fronteira real (rede, disco, processo) ou quando preciso dela para testar. Interface sem segunda implementação e sem teste que a use é só cerimônia.

Resumo da conversa