Domain Validation -  A abordagem correta - II
    Neste artigo vou continuar mostrando o porquê de fazer a validação no domínio da aplicação.

Continuando o artigo anterior onde vimos o problema de não fazer a validação no domínio.

Após mover a validação básica para o domínio, a equipe sentiu-se confiante. Quantidades inválidas? Bloqueadas. Preços negativos? Impossível. Mas então, um novo chamado chegou.

“O cliente conseguiu aplicar o mesmo desconto duas vezes.” Nenhuma exceção. Nenhum erro. Nenhum travamento. Apenas… comportamento errado.

Por que a validação básica não é suficiente

Aqui está a verdade desconfortável:  A maioria dos bugs de domínio não é sobre dados inválidos — são sobre transições inválidas. Os dados pareciam bons. A história é que estava errada.

Validação não é apenas sobre “isto é válido?”. É sobre:

Isso pode acontecer agora?
Isso é permitido após aquilo?
Isso já aconteceu?
Esta operação é idempotente?
Isso viola um invariante?

É aqui que os Agregados entram na história.

No DDD, a validação não acontece em todos os lugares. Ela acontece nos limites do agregado.

Um agregado garante a consistência dentro do seu limite. Se uma regra abrange múltiplas entidades — ela pertence à raiz do Agregado (Aggregate Root).

O agregado Pedido (revisitado)

Vamos expandir nosso agregado Pedido.

public sealed class Pedido
{
    private readonly List<ItemPedido> _itens = new();
    public PedidoId Id { get; }
    public StatusPedido Status { get; private set; }
    public IReadOnlyCollection<ItemPedido> Itens => _itens.AsReadOnly();
    private Pedido(PedidoId id)
    {
        Id = id;
        Status = StatusPedido.Rascunho;
    }
    public static Pedido Criar()
    {
        return new Pedido(PedidoId.Novo());
    }
}

Bem Simples. Mas agora vem o comportamento.

Regra: Você não pode modificar um pedido enviado

Esta não é uma regra de UI. Não é uma regra de API. É uma lei de negócio.

Vamos codificar a regra no comportamento

public void AdicionarItem(ProdutoId produtoId, Dinheiro preco, int quantidade)
{
    GarantirRascunho();
    if (quantidade <= 0)
        throw new ExcecaoDominio("A quantidade deve ser maior que zero.");
    _itens.Add(new ItemPedido(produtoId, preco, quantidade));
}
private void GarantirRascunho()
{
    if (Status != StatusPedido.Rascunho)
        throw new ExcecaoDominio("O pedido não pode ser modificado após o envio.");
}

Agora:

A regra é imposta.
Ela não pode ser esquecida.
Ela não pode ser burlada.

Validação através de transições de estado

Em vez de perguntar: “Este dado de entrada é válido?”, o domínio pergunta: “Esta transição é permitida?”.

Enviando o pedido:

public void Enviar()
{
    GarantirRascunho();
    if (!_itens.Any())
        throw new ExcecaoDominio("Não é possível enviar um pedido vazio.");
    Status = StatusPedido.Enviado;
}

Isso é validação. Mas não parece validação. Parece comportamento. Esse é o ponto.

Compare isso com a abordagem anêmica clássica:   pedido.Status = StatusPedido.Enviado;

A validação acontece… em algum outro lugar. Talvez. Às vezes. É assim que os invariantes morrem.

Objetos de Valor (Value Objects): validação que você nunca repete

Entidades protegem o estado. Objetos de Valor protegem o significado.

Dinheiro como um Objeto de Valor

public sealed record Dinheiro
{
    public decimal Quantia { get; }
    private Dinheiro(decimal quantia)
    {
        Quantia = quantia;
    }
    public static Dinheiro Criar(decimal quantia)
    {
        if (quantia < 0)
            throw new ExcecaoDominio("Dinheiro não pode ser negativo.");
            
        return new Dinheiro(quantia);
    }
    public bool EhZero => Quantia == 0;
}

Agora: dinheiro negativo não pode existir, nenhum validador é necessário no serviço, e nenhuma verificação é repetida.

A seguir vamos comparar :  Validação de Aplicação vs. Validação de Domínio

Validação de Aplicação Validação de Domínio
A requisição está bem formada?   Esta operação faz sentido?
Campos obrigatórios estão presentes? Esta transição de estado é permitida?
O formato está correto? Isso viola regras de negócio?

Elas não são intercambiáveis. O FluentValidation, por exemplo, pertence à camada de aplicação/API para proteger as fronteiras externas.

Agora vejamos a última classe de bugs: Regrasa entre agregados

Exemplos:

Um cliente só pode ter uma assinatura ativa.
Um pedido não pode ser enviado se a conta estiver suspensa.

O erro: Forçar tudo dentro de uma entidade
Injetar repositórios ou serviços dentro de entidades quebra o encapsulamento e a testabilidade.

A solução: Serviços de Domínio (Domain Services)
Um Serviço de Domínio existe quando uma regra não pertence naturalmente a uma única entidade.

Exemplo:

public interface IPoliticaAssinatura
{
    bool PodeCriarAssinatura(ClienteId clienteId);
}
public sealed class PoliticaAssinatura : IPoliticaAssinatura
{
    private readonly IRepositorioAssinatura _repositorio;
    public PoliticaAssinatura(IRepositorioAssinatura repositorio)
    {
        _repositorio = repositorio;
    }
    public bool PodeCriarAssinatura(ClienteId clienteId)
    {
        return !_repositorio.PossuiAssinaturaAtiva(clienteId);
    }
}
 

O agregado não sabe sobre o repositório. A aplicação coordena:

public async Task<Resultado> CriarAssinatura(ClienteId clienteId)
{
    if (!_politica.PodeCriarAssinatura(clienteId))
        return Resultado.Falha("O cliente já possui uma assinatura ativa.");
    var assinatura = Assinatura.Criar(clienteId);
    await _repositorio.AdicionarAsync(assinatura);
    
    return Resultado.Sucesso();
}
 

Testando a validação de domínio

Se o seu domínio não é testado, ele não está protegido.

[Fact]
public void Nao_deve_enviar_pedido_vazio()
{
    var pedido = Pedido.Criar();
    Action acao = () => pedido.Enviar();
    acao.Should().Throw<ExcecaoDominio>()
       .WithMessage("Não é possível enviar um pedido vazio.");
}
 

Modelo Mental Final

A validação não é uma camada. Validação é uma responsabilidade. E o domínio é o dono dela.

Entidades impõem invariantes.
Objetos de Valor impõem significado.
Serviços de Domínio lidam com regras entre agregados.

Quando a validação vive no domínio, o sistema torna-se honesto e a refatoração tornam-se seguros

Resumo da Arquitetura de Validação

Para visualizar como as responsabilidades se distribuem agora que o sistema está maduro, observe o fluxo de proteção:

A Fronteira (API/Aplicação): O FluentValidation e o DataAnnotations continuam sendo úteis, mas apenas para garantir que o que chega da internet não é lixo (formato de e-mail, campos obrigatórios, tipos de dados).

O Coração (Domínio): As Entidades e Objetos de Valor garantem que, uma vez que o dado entrou, ele respeite as leis do negócio. Se um Pedido diz que não pode ser alterado após o envio, essa é uma verdade absoluta em todo o sistema.

A Orquestração (Serviços de Domínio): Quando a regra é complexa demais para um único objeto, os Serviços de Domínio entram para manter a pureza sem inflar as entidades.

O Saldo Final

A longo prazo, essa abordagem traz três benefícios que pagam o investimento inicial:

Código Autodocumentado: Ao ler o método Enviar(), qualquer desenvolvedor entende as regras de negócio sem precisar procurar em manuais.

Testes que Duram: Seus testes unitários agora testam regras de negócio, e não detalhes de implementação. Eles não quebram quando você troca o banco de dados ou atualiza o framework.

Confiança: O sistema para de "deixar passar" estados inconsistentes. Como diz o ditado: "Torne os estados ilegais irrepresentáveis".

"A validação não é um obstáculo para o desenvolvimento; é a garantia de que o sistema que você construiu é, de fato, o sistema que o negócio precisa."

E estamos conversados

"Mas o Senhor Deus é a verdade; ele mesmo é o Deus vivo e o Rei eterno; ao seu furor treme a terra, e as nações não podem suportar a sua indignação."
Jeremias 10:10

Referências:


José Carlos Macoratti