O problema com as implementações que você encontra por aí
Pesquise "Clean Architecture .NET" no GitHub e você vai encontrar dezenas de repositórios de exemplo. A maioria vai ter as mesmas pastas: Domain, Application, Infrastructure, Presentation. Mas se você olhar com atenção, vai perceber que a maioria está errada — ou pelo menos incompleta.
O erro mais comum: o domínio importa pacotes NuGet. O segundo mais comum: Application depende de Infrastructure. E o terceiro, mais sutil: as camadas existem no nome dos namespaces mas não existem no código — o serviço de aplicação chama o banco direto porque "é mais simples".
Clean Architecture não é uma estrutura de pastas. É um conjunto de regras de dependência. E quando você viola essas regras, o que parece ser Clean Architecture na pasta é um monólito disfarçado por dentro.
A Dependency Rule — a única regra que importa
Robert Martin define Clean Architecture com uma regra central, a Dependency Rule:
"Source code dependencies must point only inward, toward higher-level policies."
Em termos práticos para .NET: nenhuma camada interna pode referenciar uma camada externa. O domínio não sabe que existe Entity Framework. A camada de aplicação não sabe que o banco é PostgreSQL. A infraestrutura é um detalhe — plugável e substituível.
Visualize como círculos concêntricos:
[ Presentation / API ]
[ Infrastructure ]
[ Application ]
[ Domain ]
As setas de dependência apontam sempre para dentro. Infrastructure implementa interfaces definidas em Application. Presentation chama casos de uso de Application. Domain não referencia ninguém.
As quatro camadas e o que pertence a cada uma
Domain — o núcleo imutável
Aqui vivem as regras de negócio que existiriam mesmo que não houvesse computador. Entidades, Value Objects, Aggregates, Domain Events, interfaces de repositório e exceções de domínio.
O que não pertence ao Domain: anotações do EF Core ([Table], [Column]), dependências NuGet externas (a não ser pacotes puros de C#), lógica de validação de formulário, regras de apresentação.
// Domain/Entities/Pedido.cs
public class Pedido : Entity
{
private readonly List<ItemPedido> _itens = new();
public IReadOnlyList<ItemPedido> Itens => _itens.AsReadOnly();
public StatusPedido Status { get; private set; }
public ClienteId ClienteId { get; private set; }
public Money ValorTotal { get; private set; }
// Construtor privado — só a factory cria pedidos
private Pedido() { }
public static Pedido Criar(ClienteId clienteId)
{
var pedido = new Pedido
{
ClienteId = clienteId,
Status = StatusPedido.Rascunho
};
pedido.AddDomainEvent(new PedidoCriadoEvent(pedido.Id));
return pedido;
}
public void AdicionarItem(ProdutoId produtoId, int quantidade, Money precoUnitario)
{
if (Status != StatusPedido.Rascunho)
throw new DomainException("Não é possível alterar um pedido já confirmado.");
var itemExistente = _itens.FirstOrDefault(i => i.ProdutoId == produtoId);
if (itemExistente is not null)
itemExistente.AumentarQuantidade(quantidade);
else
_itens.Add(new ItemPedido(produtoId, quantidade, precoUnitario));
RecalcularTotal();
}
private void RecalcularTotal() =>
ValorTotal = _itens.Aggregate(Money.Zero, (acc, i) => acc + i.Subtotal);
}
Note: sem atributos do EF, sem referências externas, lógica de negócio 100% dentro da entidade.
Application — casos de uso orquestrados
Orquestra o domínio para atender casos de uso específicos. Define interfaces que a infraestrutura vai implementar (IRepository, IEmailService, IUnitOfWork). Contém Commands, Queries (CQRS), Handlers, DTOs e validações de entrada.
// Application/UseCases/Pedidos/ConfirmarPedido/ConfirmarPedidoCommand.cs
public record ConfirmarPedidoCommand(Guid PedidoId, Guid ClienteId) : IRequest<Result>;
// Application/UseCases/Pedidos/ConfirmarPedido/ConfirmarPedidoHandler.cs
public class ConfirmarPedidoHandler : IRequestHandler<ConfirmarPedidoCommand, Result>
{
private readonly IPedidoRepository _pedidoRepo;
private readonly IUnitOfWork _uow;
private readonly IEventPublisher _events;
public ConfirmarPedidoHandler(
IPedidoRepository pedidoRepo,
IUnitOfWork uow,
IEventPublisher events)
{
_pedidoRepo = pedidoRepo;
_uow = uow;
_events = events;
}
public async Task<Result> Handle(
ConfirmarPedidoCommand cmd,
CancellationToken ct)
{
var pedido = await _pedidoRepo.GetByIdAsync(cmd.PedidoId, ct);
if (pedido is null)
return Result.Failure("Pedido não encontrado.");
if (pedido.ClienteId != new ClienteId(cmd.ClienteId))
return Result.Failure("Pedido não pertence a este cliente.");
pedido.Confirmar(); // lógica no domínio
await _uow.CommitAsync(ct);
await _events.PublishDomainEventsAsync(pedido, ct);
return Result.Success();
}
}
A camada de Application não sabe nada sobre SQL, HTTP ou qualquer framework. Só orquestra.
Infrastructure — implementações concretas
Implementa as interfaces de Application. Aqui moram: repositórios com EF Core, clientes HTTP, serviços de e-mail/SMS, leitura de configuração, acesso a filas. A infraestrutura depende de Application (para implementar suas interfaces), nunca o contrário.
// Infrastructure/Persistence/Repositories/PedidoRepository.cs
public class PedidoRepository : IPedidoRepository
{
private readonly AppDbContext _db;
public PedidoRepository(AppDbContext db) => _db = db;
public async Task<Pedido?> GetByIdAsync(Guid id, CancellationToken ct) =>
await _db.Pedidos
.Include(p => p.Itens)
.FirstOrDefaultAsync(p => p.Id == id, ct);
public void Add(Pedido pedido) => _db.Pedidos.Add(pedido);
}
Presentation — entrega ao mundo externo
Controllers, Minimal API endpoints, gRPC handlers, consumidores de fila. Traduz HTTP para Commands/Queries, chama Application, traduz Result para HTTP response. Não contém lógica de negócio.
// Presentation/Endpoints/PedidosEndpoints.cs
public static class PedidosEndpoints
{
public static void Map(IEndpointRouteBuilder app)
{
app.MapPost("/pedidos/{id:guid}/confirmar", async (
Guid id,
ClaimsPrincipal user,
ISender sender,
CancellationToken ct) =>
{
var clienteId = Guid.Parse(user.FindFirstValue(ClaimTypes.NameIdentifier)!);
var result = await sender.Send(new ConfirmarPedidoCommand(id, clienteId), ct);
return result.IsSuccess ? Results.NoContent() : Results.BadRequest(result.Error);
}).RequireAuthorization();
}
}
Dependency Inversion na prática: o truque do projeto compartilhado
O ponto mais confuso para quem está aprendendo: se Infrastructure implementa interfaces de Application, como registrar no DI sem violar as dependências?
A resposta é o Composition Root — um projeto de bootstrap (geralmente o próprio WebApi ou um projeto CompositionRoot separado) que referencia todos os projetos e monta o container:
// WebApi/Program.cs (Composition Root)
builder.Services
.AddApplication() // registros da camada Application
.AddInfrastructure(builder.Configuration) // registros de Infrastructure
.AddPresentation(); // middlewares, swagger, etc.
// Infrastructure/DependencyInjection.cs
public static IServiceCollection AddInfrastructure(
this IServiceCollection services,
IConfiguration config)
{
services.AddDbContext<AppDbContext>(opt =>
opt.UseNpgsql(config.GetConnectionString("Default")));
services.AddScoped<IPedidoRepository, PedidoRepository>();
services.AddScoped<IUnitOfWork, UnitOfWork>();
return services;
}
O WebApi project referencia tudo. Os projetos internos não referenciam os externos. A Dependency Rule é mantida.
Os 5 erros que destroem a Clean Architecture
1. Domain com referência ao EF Core. Acontece quando alguém coloca as configurações de mapeamento (EntityTypeConfiguration) dentro do projeto Domain. Solução: mover para Infrastructure.
2. Handler fazendo query direta no banco. O handler injeta AppDbContext em vez de um repositório. Viola a Dependency Rule — Application depende de Infrastructure. Solução: sempre usar interfaces.
3. Fat Controller. O controller tem 200 linhas com lógica de negócio. Não é Clean Architecture — é Controller-Oriented Architecture. Solução: controller chama handler, handler chama domínio.
4. Anemic Domain Model. As entidades são só bags de propriedades com getters e setters públicos. Toda lógica fica nos handlers. Isso é Transaction Script disfarçado de DDD. Solução: mover lógica para dentro das entidades.
5. Mapeamento manual em todo lugar. DTOs sendo mapeados em handlers, controllers e até repositórios. Resulta em código duplicado e difícil de manter. Solução: mappers centralizados (ex: AutoMapper ou manual em uma camada só).
Testabilidade: o benefício mais subestimado
Quando a Dependency Rule é respeitada, o domínio e a camada de Application são 100% testáveis sem banco de dados, HTTP ou qualquer infraestrutura real:
public class ConfirmarPedidoHandlerTests
{
[Fact]
public async Task Deve_confirmar_pedido_do_cliente_correto()
{
// Arrange
var clienteId = Guid.NewGuid();
var pedido = Pedido.Criar(new ClienteId(clienteId));
pedido.AdicionarItem(new ProdutoId(Guid.NewGuid()), 2, Money.Of(50m, "BRL"));
var repo = Substitute.For<IPedidoRepository>();
repo.GetByIdAsync(pedido.Id, default).Returns(pedido);
var uow = Substitute.For<IUnitOfWork>();
var events = Substitute.For<IEventPublisher>();
var handler = new ConfirmarPedidoHandler(repo, uow, events);
var cmd = new ConfirmarPedidoCommand(pedido.Id, clienteId);
// Act
var result = await handler.Handle(cmd, default);
// Assert
result.IsSuccess.Should().BeTrue();
await uow.Received(1).CommitAsync(default);
}
}
Sem banco, sem HTTP, sem tempo de setup — teste roda em milissegundos.
Quando NOT usar Clean Architecture
Clean Architecture tem custo: mais projetos, mais interfaces, mais indireção. Para um CRUD simples com 5 tabelas, pode ser over-engineering. Considere:
Use quando: domínio complexo com regras de negócio que mudam com frequência, equipe grande com múltiplos times, necessidade de substituir banco ou framework no futuro, alta cobertura de testes é requisito.
Não use quando: projeto de curto prazo, prova de conceito, aplicação CRUD sem lógica de negócio relevante, time pequeno sem familiaridade com o padrão.
Conclusão
Clean Architecture não é sobre ter as pastas certas — é sobre ter as dependências certas. A Dependency Rule é a única lei que importa: código interno nunca depende de código externo. O domínio é soberano. A infraestrutura é descartável.
Quando você aplica isso corretamente, o resultado é um sistema que você consegue testar sem banco, trocar o ORM sem reescrever regras de negócio, e escalar a equipe sem acidente de fronteiras.
Se quiser ver esses padrões funcionando em um projeto real — desde a estrutura até os testes — é exatamente o que a Neryx implementa nos projetos dos clientes.