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<ItemPedido> _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 => 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 => i.PrecoUnitario * i.Quantidade); return _itens.Count > 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 < 0 || percentual > 100) throw new DomainException("Percentual de desconto inválido."); return new Dinheiro(Valor * (1 - percentual / 100), Moeda); } public override string ToString() => $"{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) => _context = context; public async Task<Pedido?> GetByIdAsync(Guid id, CancellationToken ct = default) => await _context.Pedidos .Include(p => p.Itens) .FirstOrDefaultAsync(p => p.Id == id, ct); public async Task<IReadOnlyList<Pedido>> GetByClienteIdAsync(Guid clienteId, CancellationToken ct = default) => await _context.Pedidos .Where(p => p.ClienteId == clienteId) .Include(p => p.Itens) .ToListAsync(ct); public void Add(Pedido pedido) => _context.Pedidos.Add(pedido); public void Update(Pedido pedido) => _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: