DDD C# .NET Arquitetura

Domain-Driven Design (DDD) na prática com C#: entidades, agregados e repositórios

Aprenda a aplicar DDD em projetos .NET reais: como modelar entidades ricas, definir agregados, value objects e repositórios com exemplos em C# que você.

N
Neryx Digital Architects
27 de outubro de 2025
14 min de leitura
260 profissionais leram
Categoria: Arquitetura Público: Times de engenharia e produto Etapa: Aprendizado

Domain-Driven Design — DDD — é uma das abordagens mais citadas no desenvolvimento de software empresarial, e também uma das mais mal aplicadas. Muitos projetos adotam a nomenclatura (Entidade, Repositório, Serviço de Domínio) sem capturar a essência: o código deve falar a linguagem do negócio.

Neste guia você vai ver DDD na prática com C#, sem filosofia excessiva — só o que realmente muda no código.

O problema que o DDD resolve

Imagine um sistema de e-commerce onde o código de processamento de pedidos vive espalhado entre controllers, services e procedures de banco. Toda regra de negócio ("pedido com mais de 3 itens tem desconto", "não pode cancelar pedido já enviado") está duplicada ou enterrada em SQL.

Com o tempo, adicionar uma nova regra significa caçar onde o comportamento está e torcer para não quebrar outra coisa. DDD ataca exatamente isso: coloca as regras de negócio dentro dos objetos de domínio.

Os blocos de construção do DDD

Entidades

Uma entidade tem identidade única e ciclo de vida. Dois objetos com os mesmos atributos podem ser entidades diferentes se tiverem IDs diferentes.

public class Pedido
{
    public Guid Id { get; private set; }
    public IReadOnlyList<ItemPedido> Itens => _itens.AsReadOnly();
    public StatusPedido Status { get; private set; }
    public DateTime CriadoEm { get; private set; }
private readonly List&lt;ItemPedido&gt; _itens = new();

private Pedido() { } // EF Core

public static Pedido Criar(Guid clienteId)
{
    return new Pedido
    {
        Id = Guid.NewGuid(),
        Status = StatusPedido.Rascunho,
        CriadoEm = DateTime.UtcNow
    };
}

public void AdicionarItem(Produto produto, int quantidade)
{
    if (Status != StatusPedido.Rascunho)
        throw new DomainException("Só é possível adicionar itens em pedidos em rascunho.");

    var itemExistente = _itens.FirstOrDefault(i =&gt; i.ProdutoId == produto.Id);
    if (itemExistente != null)
        itemExistente.AumentarQuantidade(quantidade);
    else
        _itens.Add(ItemPedido.Criar(produto, quantidade));
}

public void Confirmar()
{
    if (!_itens.Any())
        throw new DomainException("Pedido sem itens não pode ser confirmado.");

    Status = StatusPedido.Confirmado;
}

public void Cancelar()
{
    if (Status == StatusPedido.Enviado || Status == StatusPedido.Entregue)
        throw new DomainException("Pedido já enviado ou entregue não pode ser cancelado.");

    Status = StatusPedido.Cancelado;
}

public decimal CalcularTotal()
{
    var subtotal = _itens.Sum(i =&gt; i.PrecoUnitario * i.Quantidade);
    return _itens.Count &gt; 3 ? subtotal * 0.95m : subtotal; // 5% de desconto acima de 3 itens
}

}

Perceba o que muda em relação a um CRUD comum: as regras de negócio ("pedido enviado não pode ser cancelado", "desconto acima de 3 itens") vivem dentro da entidade. O controller não precisa saber dessas regras — ele só chama pedido.Confirmar().

Value Objects

Value objects não têm identidade — dois value objects com os mesmos valores são iguais. São imutáveis e perfeitos para modelar conceitos como dinheiro, endereço, CPF.

public sealed record Dinheiro(decimal Valor, string Moeda)
{
    public static Dinheiro BRL(decimal valor)
    {
        if (valor < 0)
            throw new DomainException("Valor monetário não pode ser negativo.");
        return new Dinheiro(valor, "BRL");
    }
public Dinheiro Somar(Dinheiro outro)
{
    if (Moeda != outro.Moeda)
        throw new DomainException("Não é possível somar valores em moedas diferentes.");
    return new Dinheiro(Valor + outro.Valor, Moeda);
}

public Dinheiro AplicarDesconto(decimal percentual)
{
    if (percentual &lt; 0 || percentual &gt; 100)
        throw new DomainException("Percentual de desconto inválido.");
    return new Dinheiro(Valor * (1 - percentual / 100), Moeda);
}

public override string ToString() =&gt; $"{Moeda} {Valor:F2}";

}

// Uso: var preco = Dinheiro.BRL(100m); var comDesconto = preco.AplicarDesconto(10); // BRL 90.00

Com C# 9+ records, implementar value objects ficou muito simples: a igualdade por valor já vem de graça.

Agregados e Aggregate Root

Um agregado é um cluster de objetos que deve ser tratado como uma unidade para fins de consistência. O Aggregate Root é a entidade raiz — o único ponto de entrada para modificar o agregado.

No exemplo acima, Pedido é o Aggregate Root. Você nunca modifica um ItemPedido diretamente — passa pelo Pedido:

// ERRADO — viola o agregado:
var item = pedido.Itens.First();
item.AlterarQuantidade(5); // acesso direto ao item

// CORRETO — passa pelo Aggregate Root: pedido.AlterarQuantidadeItem(itemId, 5);

Isso garante que as invariantes do agregado (regras que devem ser sempre verdadeiras) sejam validadas em um único lugar.

Repositórios

O repositório é a abstração que esconde a persistência. O domínio não sabe se os dados vêm do PostgreSQL, de um arquivo ou de um mock de teste.

// Interface no domínio (não referencia EF Core)
public interface IPedidoRepository
{
    Task<Pedido?> GetByIdAsync(Guid id, CancellationToken ct = default);
    Task<IReadOnlyList<Pedido>> GetByClienteIdAsync(Guid clienteId, CancellationToken ct = default);
    void Add(Pedido pedido);
    void Update(Pedido pedido);
}

// Implementação na camada de infraestrutura (referencia EF Core) public class PedidoRepository : IPedidoRepository { private readonly AppDbContext _context;

public PedidoRepository(AppDbContext context) =&gt; _context = context;

public async Task&lt;Pedido?&gt; GetByIdAsync(Guid id, CancellationToken ct = default)
    =&gt; await _context.Pedidos
        .Include(p =&gt; p.Itens)
        .FirstOrDefaultAsync(p =&gt; p.Id == id, ct);

public async Task&lt;IReadOnlyList&lt;Pedido&gt;&gt; GetByClienteIdAsync(Guid clienteId, CancellationToken ct = default)
    =&gt; await _context.Pedidos
        .Where(p =&gt; p.ClienteId == clienteId)
        .Include(p =&gt; p.Itens)
        .ToListAsync(ct);

public void Add(Pedido pedido) =&gt; _context.Pedidos.Add(pedido);
public void Update(Pedido pedido) =&gt; _context.Pedidos.Update(pedido);

}

Domain Events

Domain events são acontecimentos relevantes para o negócio que outros contextos podem querer reagir. Eles desacoplam efeitos colaterais (enviar e-mail, atualizar estoque) da lógica principal.

// Evento de domínio
public record PedidoConfirmadoEvent(Guid PedidoId, Guid ClienteId, decimal Total) : IDomainEvent;

// Na entidade Pedido: public void Confirmar() { if (!_itens.Any()) throw new DomainException(“Pedido sem itens não pode ser confirmado.”);

Status = StatusPedido.Confirmado;
AddDomainEvent(new PedidoConfirmadoEvent(Id, ClienteId, CalcularTotal()));

}

// Handler do evento (na camada de aplicação): public class EnviarEmailConfirmacaoHandler : INotificationHandler<PedidoConfirmadoEvent> { private readonly IEmailService _emailService;

public async Task Handle(PedidoConfirmadoEvent notification, CancellationToken ct)
{
    await _emailService.EnviarConfirmacaoPedido(notification.ClienteId, notification.PedidoId);
}

}

Estrutura de projeto com DDD + Clean Architecture

src/
├── Domain/
│   ├── Entities/        # Pedido, Cliente, Produto
│   ├── ValueObjects/    # Dinheiro, Endereco, Cpf
│   ├── Events/          # PedidoConfirmadoEvent, etc.
│   ├── Repositories/    # Interfaces (IPedidoRepository)
│   └── Exceptions/      # DomainException
├── Application/
│   ├── Commands/        # ConfirmarPedidoCommand + Handler
│   ├── Queries/         # GetPedidoQuery + Handler
│   └── EventHandlers/   # EnviarEmailConfirmacaoHandler
├── Infrastructure/
│   ├── Persistence/     # AppDbContext, PedidoRepository
│   └── Services/        # EmailService, etc.
└── API/
    └── Controllers/     # PedidosController

Erros comuns ao aplicar DDD

Entidades anêmicas: criar classes com só getters/setters e mover toda a lógica para services. Isso é o oposto do DDD — o modelo de domínio não representa o negócio, é só um container de dados.

Repositório genérico demais: IRepository<T> com GetAll() parece elegante mas força o domínio a expor queries de infraestrutura. Prefira repositórios com métodos de negócio: GetPedidosPendentesDeEnvio().

Aplicar DDD em CRUDs simples: um cadastro de produto sem regras de negócio não precisa de DDD. O custo de estrutura supera o benefício. Reserve DDD para os contextos com maior complexidade de negócio.

Quando usar DDD

DDD tem custo de estrutura. Compensa quando: o domínio tem regras de negócio complexas e em constante mudança; a equipe precisa falar a mesma linguagem que o time de negócios (Ubiquitous Language); o sistema vai crescer e ser mantido por anos.

Não compensa quando: o sistema é principalmente CRUD; o time é pequeno e o prazo é curto; as regras de negócio são simples e estáveis.

Conclusão

DDD não é uma coleção de padrões técnicos — é uma forma de pensar o software centrada no negócio. Entidades ricas, value objects imutáveis e repositórios que falam a linguagem do domínio resultam em código que qualquer membro do time (técnico ou não) consegue ler e entender.

O ponto de partida prático: comece tornando suas entidades ricas. Mova as regras de negócio dos services para dentro dos objetos. O resto — eventos, agregados, bounded contexts — você vai adoptar conforme a complexidade exigir.

A Neryx aplica DDD e Clean Architecture em projetos .NET em produção. Se você está redesenhando a arquitetura de um sistema ou começando um novo, podemos ajudar a modelar o domínio corretamente desde o início.

Leitura complementar:

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.