Clean Architecture Arquitetura .NET C# DDD

Clean Architecture na prática: camadas, regras de dependência e erros comuns em projetos .NET

Guia avançado de Clean Architecture no .NET: como organizar camadas, aplicar a Dependency Rule de verdade e os erros que fazem a arquitetura virar um monólito disfarçado.

N
Neryx Digital Architects
15 de março de 2026
14 min de leitura
230 profissionais leram
Categoria: Arquitetura Público: Times de engenharia e produto Etapa: Aprendizado

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.

Precisa desenhar a próxima fase com menos retrabalho?

Fazemos discovery técnico para mapear riscos, arquitetura-alvo e sequência de execução antes de investir pesado.

Solicitar Discovery

Newsletter

Receba artigos como este no seu e-mail

Conteúdo técnico sobre arquitetura de software, .NET, IA e gestão de produto. Sem spam.