Se você já trabalhou em mais de um projeto .NET de médio ou grande porte, provavelmente já ouviu alguém dizer "isso não é Clean Architecture de verdade" durante uma revisão de código. Essa frase, dita com ar de autoridade, esconde um dos maiores problemas que a comunidade de desenvolvimento tem hoje: transformar um conjunto de princípios úteis em uma religião com regras rígidas, pastas obrigatórias e camadas que existem só para existir.
A Clean Architecture, proposta por Robert C. Martin (o "Uncle Bob"), nunca foi pensada como uma receita de bolo com nomes de pastas fixos. Ela é, na essência, um conjunto de ideias sobre como organizar dependências de código para que as partes mais importantes do seu sistema (as regras de negócio) não fiquem reféns de detalhes técnicos como banco de dados, framework web ou provedor de nuvem. O problema é que muita gente aprendeu o desenho do diagrama de círculos concêntricos sem entender o motivo por trás dele, e aí a arquitetura vira burocracia.
Neste post eu quero desmontar esse dogmatismo. Vamos entender o porquê da Clean Architecture existir, como aplicá-la de forma pragmática em projetos .NET, quando ela realmente compensa e, mais importante, quando ela é um tiro no pé.
O problema que a Clean Architecture resolve
Antes de falar de camadas, vale entender o problema original. Em sistemas mal estruturados, é comum que a lógica de negócio fique espalhada dentro de controllers, misturada com chamadas ao Entity Framework, validações de formulário e regras de autorização, tudo no mesmo arquivo. Isso funciona bem no começo, mas cria dois problemas sérios conforme o sistema cresce.
O primeiro é o acoplamento a detalhes técnicos. Se a regra de negócio "um pedido só pode ser cancelado se ainda não foi enviado" está escrita dentro de uma query LINQ acoplada ao DbContext, trocar de banco de dados, escrever um teste unitário rápido ou reaproveitar essa regra em outro contexto (um job em background, por exemplo) se torna doloroso.
O segundo problema é a dificuldade de testar. Regras de negócio misturadas com infraestrutura (HTTP, banco, filas) exigem testes de integração pesados para validar coisas que deveriam ser simples testes unitários, rodando em milissegundos, sem subir banco nem servidor.
A Clean Architecture ataca esses dois problemas com uma ideia central chamada regra de dependência: as camadas mais internas (as regras de negócio) não podem depender das camadas mais externas (frameworks, banco de dados, UI). É o inverso do que normalmente acontece em código legado, onde tudo depende de tudo.
A regra de dependência na prática
Esqueça por um momento os nomes exatos das camadas. O que importa é a direção das setas. Numa aplicação .NET típica, você normalmente tem:
- Domínio: entidades, regras de negócio, interfaces que descrevem o que o sistema precisa (sem dizer como).
- Aplicação: casos de uso que orquestram o domínio para resolver um cenário específico (criar pedido, cancelar assinatura, etc).
- Infraestrutura: implementações concretas de acesso a dados, envio de e-mail, integração com APIs externas.
- Apresentação: controllers de API, endpoints minimal API, jobs, workers, o que quer que seja a porta de entrada do sistema.
A regra é simples: Domínio não conhece Aplicação, Aplicação não conhece Infraestrutura nem Apresentação, e as dependências concretas de Infraestrutura são injetadas via interfaces definidas no Domínio ou na Aplicação. Isso é o famoso Dependency Inversion Principle aplicado em escala arquitetural.
Vamos ver isso em código.
// Camada de Domínio: Order.cs
public class Order
{
public Guid Id { get; private set; }
public OrderStatus Status { get; private set; }
public Order(Guid id)
{
Id = id;
Status = OrderStatus.Pending;
}
public void Cancel()
{
if (Status == OrderStatus.Shipped)
{
throw new InvalidOperationException(
"Não é possível cancelar um pedido já enviado.");
}
Status = OrderStatus.Cancelled;
}
}
public enum OrderStatus
{
Pending,
Shipped,
Cancelled
}
Repare que a entidade Order não sabe nada sobre Entity Framework, HTTP ou banco de dados. A regra de negócio "não pode cancelar pedido enviado" está dentro do próprio objeto, o que garante que ninguém consiga burlar essa regra chamando o setter diretamente de outro lugar do código. Isso é o que se chama de modelo rico, em oposição a um modelo anêmico (só propriedades públicas sem comportamento).
// Camada de Domínio: IOrderRepository.cs
public interface IOrderRepository
{
Task<Order?> GetByIdAsync(Guid id, CancellationToken ct);
Task SaveAsync(Order order, CancellationToken ct);
}
A interface IOrderRepository fica no domínio (ou na camada de aplicação, dependendo do projeto), mas quem implementa essa interface é a infraestrutura. Isso é o coração da inversão de dependência: o domínio define o contrato do que precisa, e a infraestrutura obedece esse contrato, nunca o contrário.
// Camada de Aplicação: CancelOrderHandler.cs
public class CancelOrderHandler
{
private readonly IOrderRepository _repository;
public CancelOrderHandler(IOrderRepository repository)
{
_repository = repository;
}
public async Task HandleAsync(Guid orderId, CancellationToken ct)
{
var order = await _repository.GetByIdAsync(orderId, ct)
?? throw new KeyNotFoundException("Pedido não encontrado.");
order.Cancel();
await _repository.SaveAsync(order, ct);
}
}
Esse CancelOrderHandler representa um caso de uso: ele orquestra a busca do pedido, chama a regra de negócio e persiste o resultado. Ele não sabe se o repositório usa Entity Framework, Dapper ou um arquivo JSON. Essa camada de aplicação é o que muita gente chama de "use case" ou "interactor".
// Camada de Infraestrutura: EfOrderRepository.cs
public class EfOrderRepository : IOrderRepository
{
private readonly AppDbContext _context;
public EfOrderRepository(AppDbContext context)
{
_context = context;
}
public async Task<Order?> GetByIdAsync(Guid id, CancellationToken ct) =>
await _context.Orders.FindAsync(new object[] { id }, ct);
public async Task SaveAsync(Order order, CancellationToken ct)
{
_context.Orders.Update(order);
await _context.SaveChangesAsync(ct);
}
}
Aqui, sim, entra o Entity Framework. E é só aqui que ele deveria aparecer. Se amanhã a equipe decidir trocar EF Core por Dapper para melhorar performance em uma consulta específica, só essa classe muda. O domínio e a aplicação continuam intocados, e os testes unitários deles continuam passando sem alteração nenhuma.
// Program.cs (composição das dependências)
builder.Services.AddScoped<IOrderRepository, EfOrderRepository>();
builder.Services.AddScoped<CancelOrderHandler>();
O Program.cs (ou um Startup.cs, se o projeto ainda usar o modelo antigo) é o único lugar do sistema que conhece tanto a interface quanto a implementação concreta. É aqui que a "mágica" da inversão de dependência é resolvida em tempo de execução, geralmente com o contêiner de injeção de dependência nativo do ASP.NET Core.
Onde o dogmatismo aparece (e atrapalha)
Até aqui, tudo parece razoável. O problema começa quando essas ideias viram regras absolutas aplicadas sem pensar no contexto. Alguns sintomas clássicos de dogmatismo em Clean Architecture:
Camadas demais para um CRUD simples. Se você está construindo um microsserviço que só expõe operações básicas de CRUD para uma tabela de configurações, criar quatro projetos separados (Domain, Application, Infrastructure, WebApi), cada um com sua própria pasta de interfaces, é over-engineering puro. Você vai gastar mais tempo navegando entre arquivos do que escrevendo lógica de negócio, porque simplesmente não existe lógica de negócio complexa o suficiente para justificar a separação.
Repositório genérico em cima do Entity Framework. É comum ver times criando uma interface IRepository<T> com métodos GetAll, GetById, Add, Update, Delete, implementada sobre o DbContext. Isso soa como "seguir Clean Architecture", mas na prática só duplica a API que o próprio EF Core já oferece, sem trazer nenhum benefício real de testabilidade ou flexibilidade, já que o DbContext em si já pode ser abstraído ou substituído em testes usando um provedor in-memory ou o SQLite in-memory.
Mapear DTOs em todas as direções, para tudo. Alguns times insistem que cada camada precisa ter seu próprio conjunto de objetos (entidade de domínio, modelo de aplicação, DTO de infraestrutura, view model de apresentação), mesmo quando esses objetos são idênticos. Isso gera uma quantidade enorme de código de mapeamento que não agrega valor nenhum, só trabalho manual e mais uma fonte de bugs quando alguém esquece de atualizar um mapeamento depois de adicionar um campo novo.
Confundir Clean Architecture com DDD tático obrigatório. Value Objects, Aggregate Roots, Domain Events, tudo isso são ferramentas do Domain-Driven Design que podem coexistir muito bem com Clean Architecture, mas não são requisito. Você pode ter uma Clean Architecture perfeitamente válida com entidades simples e sem nenhum conceito tático de DDD, se o domínio do seu negócio não for complexo o suficiente para justificar isso.
O ponto central é: cada camada extra, cada abstração, cada interface tem um custo de manutenção e de compreensão. Esse custo só vale a pena quando ele compra alguma coisa em troca (testabilidade, flexibilidade para trocar tecnologia, isolamento de regras de negócio complexas). Se a resposta para "por que essa camada existe?" for "porque é assim que se faz Clean Architecture", é sinal de dogma, não de engenharia.
Um guia prático de decisão
Uma forma útil de pensar nisso é avaliar a complexidade real do seu domínio de negócio, e não o tamanho do projeto em termos de infraestrutura.
Use uma estrutura em camadas completa, com Domínio, Aplicação e Infraestrutura bem separados, quando o sistema tem regras de negócio não triviais que provavelmente vão mudar com frequência, quando múltiplas interfaces de entrada (API REST, gRPC, worker de fila) precisam reutilizar a mesma lógica, ou quando existe uma perspectiva real de trocar de tecnologia de persistência ou de mensageria no futuro próximo.
Prefira uma abordagem mais enxuta, como um projeto único com pastas organizadas por feature (o modelo conhecido como Vertical Slice Architecture), quando o sistema é majoritariamente CRUD, quando o time é pequeno e a velocidade de entrega importa mais do que a flexibilidade teórica, ou quando o domínio de negócio é simples o suficiente para caber inteiro na cabeça de qualquer desenvolvedor sem precisar de uma separação formal.
Vale lembrar que essas duas abordagens não são mutuamente exclusivas dentro de um mesmo sistema maior. É perfeitamente razoável ter um monólito modular onde alguns módulos (o de faturamento, por exemplo, com regras complexas de imposto e parcelamento) sigam uma Clean Architecture rigorosa, enquanto outros módulos (um catálogo simples de produtos) sejam implementados como um CRUD direto, sem camadas extras. A arquitetura não precisa ser uniforme no sistema inteiro; ela precisa ser adequada a cada parte dele.
Testando sem precisar de todas as camadas
Um dos maiores ganhos reais da Clean Architecture, quando bem aplicada, é a facilidade de escrever testes unitários rápidos para as regras de negócio, sem precisar de banco de dados nem de servidor HTTP rodando.
public class OrderTests
{
[Fact]
public void Cancel_QuandoPedidoJaFoiEnviado_DeveLancarExcecao()
{
var order = new Order(Guid.NewGuid());
order.MarkAsShipped(); // método auxiliar de teste, hipotético
var act = () => order.Cancel();
Assert.Throws<InvalidOperationException>(act);
}
}
Note que esse teste não usa mocks, não usa banco em memória, não sobe nenhum servidor. Ele testa exatamente a regra de negócio, isolada, em milissegundos. Esse é o retorno concreto do investimento em separar o domínio de detalhes técnicos: se o seu domínio depende de DbContext ou de HttpClient, você perde essa velocidade e simplicidade de teste, e é exatamente esse tipo de dor que costuma convencer times a adotar os princípios da Clean Architecture, ainda que sem seguir o dogma dos nomes exatos de pastas.
Armadilhas comuns e como evitá-las
Mesmo quando a intenção é boa, algumas armadilhas aparecem com frequência em projetos reais. Uma delas é vazar detalhes de infraestrutura para o domínio através de atributos. É comum ver entidades de domínio cheias de atributos do Entity Framework ([Key], [ForeignKey], [Column]) ou de bibliotecas de serialização JSON. Isso quebra a promessa de que o domínio não depende de infraestrutura. Prefira configurar o mapeamento do EF Core via Fluent API, em classes separadas na camada de infraestrutura, mantendo a entidade de domínio limpa.
Outra armadilha é criar abstrações "por precaução", pensando em uma troca de tecnologia que talvez nunca aconteça. Se o projeto usa PostgreSQL desde o início e não existe nenhum plano real de trocar de banco, criar uma camada de abstração elaborada só para essa eventualidade hipotética raramente compensa o custo de manutenção. A regra prática do "You Aren't Gonna Need It" (YAGNI) se aplica muito bem aqui.
Por fim, vale prestar atenção ao tamanho dos casos de uso. Handlers de aplicação que crescem demais, orquestrando dezenas de passos e chamando múltiplos repositórios, geralmente são sinal de que uma regra de negócio que deveria estar dentro da entidade de domínio foi parar na camada de aplicação por acomodação. Sempre que possível, empurre a lógica de decisão para dentro do próprio objeto de domínio, e deixe a camada de aplicação apenas orquestrando chamadas, sem tomar decisões de negócio.
Conclusão
A Clean Architecture não é um conjunto de pastas obrigatórias nem um selo de qualidade que se ganha por seguir um diagrama à risca. Ela é uma ferramenta para resolver um problema específico: evitar que regras de negócio fiquem reféns de detalhes técnicos que mudam com mais frequência do que as regras em si. Quando esse problema existe de verdade no seu contexto, seja porque o domínio é complexo, seja porque existem múltiplas formas de entrada no sistema, seja porque a testabilidade rápida importa muito para o time, os princípios da Clean Architecture (principalmente a inversão de dependência) trazem um ganho real e mensurável.
Quando esse problema não existe, forçar a estrutura de qualquer jeito só adiciona complexidade acidental, mais arquivos para navegar, mais mapeamentos manuais e mais tempo de onboarding para quem entra no time. O verdadeiro sinal de maturidade arquitetural não é conseguir citar de cor os nomes das camadas do diagrama de Uncle Bob, mas sim saber avaliar, projeto a projeto, e até módulo a módulo dentro do mesmo projeto, onde vale a pena pagar o custo dessas camadas e onde é melhor manter as coisas simples. Arquitetura de software boa é, no fim das contas, sobre fazer escolhas conscientes e justificáveis, não sobre seguir regras à risca.