|
Neste artigo vou mostrar como aplicar os fundamentos do DDD para criar modelos expressivos e orientados a negócio. |
Princípios do DDD
Aplicados em .NET
1. Modele com significado: as classes
de domínio devem representar conceitos do negócio, não apenas estruturas
técnicas.
2. Linguagem Ubíqua: os nomes devem refletir o
vocabulário dos especialistas de domínio. Se o analista de negócios fala em
"Pedido", "Item" e "Cliente", o código deve usar exatamente esses termos —
não PedidoEntity, ItemLinha ou ClienteRegistro.
3.
Isolamento do domínio: a lógica de negócio deve residir em sua própria
camada, livre de dependências de frameworks ou banco de dados.
Regra de Dependência: As dependências sempre apontam em direção ao
domínio (núcleo), nunca para fora. A camada de infraestrutura conhece o domínio;
o domínio não conhece a infraestrutura.
Separação de
Responsabilidades
Um dos pilares do DDD é reconhecer que
diferentes partes do sistema têm naturezas completamente distintas e não devem
se misturar:
O que o negócio exige — regras,
invariantes, comportamentos: "um pedido não pode ter itens com quantidade zero",
"um e-mail precisa ter formato válido".
Como o sistema é acionado
— HTTP, filas, CLI, eventos: receber uma requisição REST e devolver uma
resposta JSON.
Como os dados são persistidos — SQL,
NoSQL, arquivos: gravar e recuperar entidades do banco de dados.
Como os casos de uso são orquestrados — coordenar agregados,
repositórios e serviços para executar uma operação completa.
Misturar
essas responsabilidades em uma única classe (como colocar queries SQL dentro de
um controller, ou regras de negócio dentro de um DbContext) cria código difícil
de testar, difícil de evoluir e altamente acoplado a detalhes técnicos.
Arquitetura
em Camadas
Para separar essas responsabilidades de forma
sistemática, o DDD organiza o sistema em quatro camadas com propósitos bem
definidos:
| Camada | Responsabilidade | Exemplos em .NET |
| Domain | Regras de negócio, Agregados, Objetos de valor, Contratos de Repositório |
Pedido, Email, IPedidoRepository |
| Application | Casos de uso , orquestração de agregados e repositórios |
CriarPedido,
CancelarPedido Commands , Handlers |
| Infrastructure | Implementação de persistência, Integrações externas, serviços de infraestrutura |
AppDbContext,
EmailService ProdutoRepository |
| Presentation | Entrada do sistema: HTTP, filas, traduz requisições em casos de uso |
Controllers, Minimal
Apis, Consumers de fila |
Cada camada conhece apenas o que precisa para cumprir seu papel. Um controller não sabe como um pedido é salvo. Um repositório não sabe quais são as regras de negócio. Essa separação é o que torna cada parte testável e substituível de forma independente.
Regra de
Dependência
Com as camadas definidas, surge uma pergunta
natural: quem pode depender de quem?
A resposta é a Regra de
Dependência: as dependências de código sempre apontam de fora para
dentro, em direção ao domínio. Camadas externas conhecem as internas; camadas
internas nunca conhecem as externas.

O Domain é o núcleo — não referencia nenhuma outra camada. Ele define contratos (interfaces) que a Infrastructure implementa, mas nunca depende diretamente dessas implementações. Essa inversão de dependência é o que garante que a lógica de negócio permanece isolada de detalhes técnicos.
O que são
Objetos de Valor (Value Objects)
Um Objeto de Valor
é um conceito do domínio definido inteiramente pelo seu conteúdo, não por uma
identidade. Dois Objetos de Valor com os mesmos dados são completamente
equivalentes — assim como dois objetos DateTime representando 2024-01-01 são
iguais, independentemente de serem instâncias diferentes.
Características
fundamentais de um Objeto de Valor:
1- Sem identidade
própria: não possui ID nem chave primária.
2- Imutabilidade: uma vez criado,
seu estado não muda. Se precisar de um valor diferente, cria-se uma nova
instância.
3- Igualdade por valor: dois objetos são iguais se todos os seus
dados forem iguais.
4- Autovalidade: encapsula suas próprias regras de
validação — um Email inválido nunca chega a existir.
Exemplos típicos de
Objetos de Valor: Email, Cpf, Endereco, Dinheiro, Intervalo, Coordenada.
Afinal o que é um record ?
Um record é
um tipo de referência introduzido no C# 9 que fornece, por padrão:
a-
Igualdade estrutural (por valor): dois records são iguais se todos os seus
membros forem iguais — sem precisar sobrescrever Equals e GetHashCode.
b-
Imutabilidade via init: propriedades declaradas com { get; init; } só podem ser
definidas durante a inicialização.
c- ToString() legível: gera
automaticamente uma representação textual com os nomes e valores das
propriedades.
d- Desconstrução: permite extrair valores com var (x, y) =
ponto;.
Por essas razões, record é a escolha natural para implementar
Objetos de Valor em C# — a linguagem já entrega os comportamentos que o DDD
exige.
Exemplo : record Email
public record Email { public string Valor { get; } private Email(string valor) => Valor = valor; // Método de fábrica: garante que instâncias inválidas nunca existam public static Email Criar(string valor) { if (!Regex.IsMatch(valor, @"^[^@]+@[^@]+\.[^@]+$")) throw new ArgumentException("E-mail inválido", nameof(valor)); return new Email(valor); } public override string ToString() => Valor;
}
|
O construtor privado
impede a criação direta de instâncias inválidas. O método de fábrica
Criar é o único ponto de entrada, garantindo que qualquer instância de
Email já é, por construção, um e-mail válido.
Por que usar record e não
class?
Com class, seria necessário sobrescrever
Equals, GetHashCode e == manualmente para obter igualdade por valor.
Com record, o compilador gera tudo isso automaticamente — e
ainda impede mutação acidental.
O que são
Aggregates ou Agregados ?
Um Agregado é um
conjunto de objetos de domínio — entidades e Objetos de Valor — tratados como
uma unidade consistente. Todo Agregado possui uma Raiz de Agregado (Aggregate
Root): a única entidade através
da qual objetos externos podem
interagir com o Agregado.
Características
fundamentais de um Agregado:
Fronteira de consistência:
todas as invariantes de negócio são garantidas dentro do Agregado. Nenhuma regra
que envolva seus membros pode ser violada.
Acesso exclusivo pela raiz:
objetos externos não seguram referências diretas a entidades internas. Toda
interação passa pela raiz.
Identidade própria: ao contrário
dos Objetos de Valor, a raiz possui um ID único.
Persistência atômica:
o Agregado inteiro é salvo ou carregado de uma vez — não partes isoladas.
Exemplo : Agregado Pedido
public sealed class Pedido { private readonly List<ItemPedido> _itens = new(); private readonly List<IEventoDominio> _eventos = new(); public Guid Id { get; private set; } = Guid.NewGuid(); public Email EmailCliente { get; private set; } public IReadOnlyCollection<ItemPedido> Itens => _itens.AsReadOnly(); public IReadOnlyCollection<IEventoDominio> Eventos => _eventos.AsReadOnly(); // Construtor protegido: permite que o EF Core instancie a entidade via reflexão protected Pedido() { } public Pedido(Email emailCliente) { EmailCliente = emailCliente ?? throw new ArgumentNullException(nameof(emailCliente)); } public void AdicionarItem(string idProduto, int quantidade) { if (quantidade <= 0) throw new InvalidOperationException("Quantidade inválida"); _itens.Add(new ItemPedido(idProduto, quantidade)); _eventos.Add(new ItemAdicionadoAoPedido(Id, idProduto, quantidade)); } public void LimparEventos() => _eventos.Clear(); } public record ItemPedido(string IdProduto, int Quantidade); |
Por que
ItemPedido é um record?
ItemPedido representa uma linha do
pedido definida pelo seu conteúdo: qual produto e qual quantidade. Não existe a
noção de "este ItemPedido específico" — apenas "um item com
ProdutoId X e Quantidade Y".
Portanto, ele se comporta como um Objeto de Valor e record é a escolha semântica correta: igualdade por valor, imutabilidade e código conciso sem boilerplate.
O Agregado Pedido controla sua consistência interna e expõe operações válidas. O construtor protected (sem parâmetros) existe exclusivamente para que o EF Core consiga materializar a entidade a partir do banco de dados — ele nunca deve ser chamado diretamente pelo código de aplicação.
Anti-Padrões
Comuns no DDD
❌ Usar o EF Core diretamente como modelo de
domínio
Evite colocar lógica de negócio em DbContext ou em entidades do EF.
Crie um modelo de domínio separado.
❌ Lógica em controllers ou serviços
de infraestrutura
Controllers devem orquestrar, não decidir. Todas as
decisões de negócio devem residir no domínio ou nos casos de uso.
❌
Injeção de dependência dentro de entidades de domínio
Entidades não devem ter
dependências de serviços externos como DbContext, HttpClient, etc.
❌ Modelo Anêmico (Anemic Domain Model)
Ocorre quando as classes de
domínio possuem apenas propriedades sem comportamento, e toda a lógica fica em
serviços externos. Isso viola o encapsulamento e é um dos anti-padrões mais
comuns em aplicações .NET.
Exemplos:
// ❌ Modelo anêmico — evite public class Pedido { public Guid Id { get; set; } public List<ItemPedido> Itens { get; set; } = new(); } // Lógica de negócio vazando para um serviço externo public class ServicoPedido { public void AdicionarItem(Pedido pedido, ItemPedido item) { ... } } // ✅ Modelo rico — prefira public class Pedido { ... // A lógica pertence à entidade public void AdicionarItem(string idProduto, int quantidade) { ... } } |
O que são
Casos de Uso ?
Um Caso de Uso (ou Use Case)
representa uma intenção do usuário ou do sistema que o software deve atender.
Ele descreve uma operação completa do ponto de vista do ator: "Criar um
Pedido", "Cancelar uma Reserva", "Aprovar um Orçamento".
No DDD e na
Arquitetura Limpa, os Casos de Uso:
- Residem na camada de Aplicação —
acima do domínio, abaixo da apresentação.
- Orquestram entidades, Agregados e
Repositórios para executar a operação.
- Não contêm regras de negócio — essas
pertencem ao domínio.
- Não conhecem detalhes de infraestrutura — apenas
interfaces (contratos).
- São o ponto de entrada para qualquer operação que
modifica estado.

Exemplo:
public interface ICriarPedidoCasoDeUso { Task Executar(CriarPedidoDto dto); } public class CriarPedidoCasoDeUso : ICriarPedidoCasoDeUso { private readonly IRepositorioPedido _repositorio; private readonly IPublicadorEventos _publicadorEventos; public CriarPedidoCasoDeUso( IRepositorioPedido repositorio, IPublicadorEventos publicadorEventos) { _repositorio = repositorio; _publicadorEventos = publicadorEventos; } public async Task Executar(CriarPedidoDto dto) { // Orquestra o domínio — as regras estão no Agregado var pedido = new Pedido(Email.Criar(dto.Email)); foreach (var item in dto.Itens) pedido.AdicionarItem(item.IdProduto, item.Quantidade); await _repositorio.Salvar(pedido); // Publica eventos de domínio após persistência bem-sucedida foreach (var evento in pedido.Eventos) await _publicadorEventos.Publicar(evento); pedido.LimparEventos(); } } |
O Caso de Uso orquestra as entidades, mas não contém regras de negócio — estas residem no Agregado.
O que são
Repositórios de Domínio ?
Um Repositório é uma
abstração que simula uma coleção em memória de Agregados, escondendo
completamente os detalhes de como eles são armazenados e recuperados. Do ponto
de vista do domínio, um repositório é apenas um lugar onde você guarda e busca
Agregados — sem saber se por baixo existe SQL Server, PostgreSQL, um arquivo
JSON ou um mock em testes.
Por que usar Repositórios no DDD?
a- Isolamento do domínio: o modelo de negócio não sabe nada sobre
banco de dados, ORM ou queries SQL.
b- Testabilidade: é trivial substituir o
repositório real por um fake em memória nos testes unitários.
c- Fronteira
arquitetural clara: o contrato (interface) pertence ao domínio; a implementação
pertence à infraestrutura.
d- Linguagem ubíqua: os métodos refletem operações
do negócio (Salvar, ObterPorId), não operações técnicas (INSERT, SELECT).
Importante: um Repositório existe apenas para Raízes de
Agregado. Não se cria repositório para ItemPedido — ele é acessado
exclusivamente através de Pedido.
Exemplo:
// Esta interface pertence ao núcleo do domínio public interface IRepositorioPedido { Task Salvar(Pedido pedido); Task<Pedido?> ObterPorId(Guid id); Task<IReadOnlyList<Pedido>> Listar(IEspecificacao<Pedido> especificacao); } |
// Esta classe pertence à infraestrutura — o domínio nunca a referencia diretamente public class RepositorioPedidoEf : IRepositorioPedido { private readonly AppDbContext _contexto; public RepositorioPedidoEf(AppDbContext contexto) => _contexto = contexto; public async Task Salvar(Pedido pedido) { _contexto.Pedidos.Add(pedido); await _contexto.SaveChangesAsync(); } public async Task<Pedido?> ObterPorId(Guid id) => await _contexto.Pedidos .Include(p => p.Itens) .FirstOrDefaultAsync(p => p.Id == id); public async Task<IReadOnlyList<Pedido>> Listar(IEspecificacao<Pedido> especificacao) => await _contexto.Pedidos .Where(especificacao.Criterio) .ToListAsync(); } |
O contrato pertence ao núcleo do domínio; a implementação com EF Core reside na infraestrutura. Essa separação é o que permite testar o domínio sem banco de dados e trocar o mecanismo de persistência sem alterar uma linha de código de negócio.
Testando o
Domínio
Uma das consequências mais valiosas de um domínio bem
modelado é a testabilidade natural. Quando a lógica de negócio está encapsulada
em Agregados e Objetos de Valor — sem dependências de banco de
dados, frameworks ou serviços externos — ela pode ser verificada de forma
direta, rápida e determinística.
Testar o domínio é importante por razões que
vão além da cobertura de código:
As regras de negócio são o ativo mais
valioso do sistema. Um e-mail inválido aceito, uma quantidade negativa
processada ou uma invariante violada podem causar dados incorretos, falhas em
cascata ou prejuízo direto ao negócio. Testes de domínio são a rede de segurança
dessas regras.
Documentação viva. Um definido como
Pedido_DeveLancarExcecao_QuandoQuantidadeForInvalida comunica
uma regra de negócio de forma inequívoca — mais clara do que qualquer comentário
no código.
Feedback imediato. Testes unitários de
domínio executam em milissegundos. Não dependem de banco de dados configurado,
container em execução ou ambiente específico. Qualquer desenvolvedor pode rodar
a suíte completa localmente em segundos.
Refatoração segura.
O domínio evolui junto com o negócio. Ter testes que cobrem os comportamentos —
não os detalhes de implementação — permite reorganizar o código interno dos
Agregados com confiança de que as regras continuam sendo respeitadas.
Separação de camadas validada. Se for difícil testar uma regra
de negócio sem instanciar um DbContext ou mockar um HttpClient, isso é um sinal
claro de que a lógica vazou para a camada errada.
O que testar no
domínio?
Testes de domínio devem focar em comportamentos, não em
getters e setters.
As categorias mais
relevantes são:
Operações válidas: o Agregado executa
corretamente quando recebe entradas dentro das regras.
Violações de
invariantes: o Agregado rejeita operações que quebrariam sua
consistência interna.
Validação de Objetos de Valor:
instâncias inválidas nunca são criadas.
Eventos de domínio:
operações relevantes produzem os eventos esperados.
Exemplos:
[Fact] public void Pedido_DeveAdicionarItem() { var pedido = new Pedido(Email.Criar("usuario@exemplo.com")); pedido.AdicionarItem("produto-1", 2);
Assert.Single(pedido.Itens); Assert.Equal(2, pedido.Itens.First().Quantidade); } [Fact] public void Pedido_DeveLancarExcecao_QuandoQuantidadeForInvalida() { var pedido = new Pedido(Email.Criar("usuario@exemplo.com")); Assert.Throws<InvalidOperationException>( () => pedido.AdicionarItem("produto-1", 0)); } [Fact] public void Email_DeveLancarExcecao_QuandoFormatoForInvalido() { Assert.Throws<ArgumentException>( () => Email.Criar("nao-e-um-email")); } |
Cada teste valida uma regra de negócio específica — sem banco de dados, sem mocks de infraestrutura, sem configuração de ambiente. Essa simplicidade é consequência direta de um domínio bem isolado: quando a lógica está no lugar certo, testá-la é trivial.
Conclusão
Aplicar DDD na plataforma .NET permite construir aplicações
centradas no negócio, capazes de evoluir de forma coerente e resilientes a
mudanças.
Embora exija mais disciplina e aprendizado inicial, os
benefícios em manutenibilidade, clareza e expressividade superam amplamente o
custo.
Para ir além, você pode:
- Estender o modelo com
Especificações, Eventos de Domínio ou Contextos Delimitados (Bounded
Contexts)
- Combinar com CQRS para separar leituras de escritas
-
Automatizar os testes de domínio com cobertura completa de invariantes
-
Combinar com uma abordagem orientada ao núcleo (Clean Architecture) para isolar
ainda mais o domínio
Seu domínio não é um detalhe técnico — é a razão
pela qual seu software existe.
E estamos conversados
"Nada façais por contenda ou por
vanglória, mas por humildade; cada um considere os outros superiores a si
mesmo."
Filipenses 2:3
Referências: