DDD Fundamentos - Modelando domínios ricos
    Neste artigo vou mostrar como aplicar os fundamentos do DDD para criar modelos expressivos e orientados a negócio.

O Domain-Driven Design (DDD) é uma estratégia de design de software que foca no núcleo de uma aplicação: seu modelo de domínio. Ao separar explicitamente "o que o sistema faz" de "como ele faz", o DDD viabiliza soluções manuteníveis, evolutivas e alinhadas ao 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:


José Carlos Macoratti