Domain Validation -  A abordagem correta - I
    Neste artigo veremos o porque de fazer a validação no domínio da aplicação.

Vamos imaginar um sistema onde um bug passou por todas as validações feitas.



“A requisição era válida.” Era o que os logs diziam. Era o que a API retornava. E, ainda assim — a regra de negócio estava errada.

Era para ser uma implantação normal. Pipelines verdes. Todos os testes passando. O Swagger parecia perfeito. Então, o ticket de suporte chegou:

“O cliente conseguiu fazer um pedido com um valor total negativo.”

No início, todos riram. “Isso é impossível.” Nós tínhamos aplicado:

E ainda assim — o impossível aconteceu.  Como ????

A verdade desconfortável

Após rastrear a requisição até o fim, a equipe descobriu algo doloroso: Nós estávamos validando dados — não regras. E essas duas coisas não são a mesma coisa.

O que a maioria das aplicações .NET chama de “validação”

Sejamos honestos. A maioria das aplicações faz algo parecido com isso:

public class CriarPedidoRequisicao
{
    [Required]
    public Guid ClienteId { get; set; }
    [Range(1, int.MaxValue)]
    public int Quantidade { get; set; }
    [Range(0.01, double.MaxValue)]
    public decimal Preco { get; set; }
}

Isso parece responsável. Parece correto. Parece... seguro. Mas não é validação de domínio.

Temos aqui o uso de Data Annotations diretamente em um DTO (Objeto de Transferência de Dados).

O exemplo verifica apenas se o dado "parece" certo.

[Required]: Garante que o ID não é nulo.

[Range]: Garante que o número é positivo.

Isso é o que chamamos de validação de contrato. Ela é importante para impedir que lixo entre na sua API, mas ela não conhece as regras de negócio.

O Problema da "Fuga de Lógica"

Quando você confia apenas nessas anotações, a sua regra de negócio (ex: "um pedido não pode ter preço zero") fica pendurada em uma classe de transporte (CriarPedidoRequisicao) e não na sua entidade Pedido.

Se amanhã você criar um pedido vindo de uma fila de mensagens (RabbitMQ) ou de uma tarefa agendada, você não usará esse DTO da API. O resultado? Você terá que duplicar as regras de validação ou correr o risco de criar um pedido inválido no banco de dados.

Por que esta validação falhou com o negócio?

Pergunte a si mesmo:

A quantidade pode ser alterada depois?
O preço pode ser sobrescrito?
Os descontos podem exceder o total?
Pode existir um pedido sem itens?
Um cliente pode fazer dois pedidos ativos ao mesmo tempo?

Nada disso é respondido aqui. Porque... Atributos validam o formato. O domínio valida o significado.

O primeiro princípio da validação de domínio

Aqui está a regra que muda tudo: Se uma regra é importante para o negócio, ela deve viver no domínio.

Não em: Controllers, DTOs, Validadores, Middleware

Apenas o domínio tem permissão para decidir o que é válido.

Então, o que “validação de domínio” realmente significa ? 

A validação de domínio responde a perguntas como:

Esta entidade tem permissão para existir neste estado?
Esta transição é legal?
Esta operação faz sentido?
Isso viola um invariante de negócio?

Estas são regras de comportamento, não regras de formatação. Conheça a entidade de domínio (feita da forma certa)

public sealed class Pedido
{
    private readonly List<ItemPedido> _itens = new();
    public PedidoId Id { get; }
    public ClienteId ClienteId { get; }
    public IReadOnlyCollection<ItemPedido> Itens => _itens.AsReadOnly();
    public Dinheiro Total => _itens.Sum(i => i.Subtotal);
    private Pedido(PedidoId id, ClienteId clienteId)
    {
        Id = id;
        ClienteId = clienteId;
    }
    public static Pedido Criar(ClienteId clienteId)
    {
        if (clienteId is null)
            throw new ExcecaoDominio("O cliente é obrigatório.");
        return new Pedido(PedidoId.Novo(), clienteId);
    }
}

Observe algo importante:

❌ Sem setters (propriedades de escrita)
❌ Sem construtores vazios
❌ Sem atributos de validação

A entidade protege a si mesma
Validação é feita através da construção

Aqui está a mudança fundamental: Objetos inválidos não devem ser criados.

Em vez de criar um objeto e depois validá-lo nós validamos enquanto criamos adicionando comportamento com regras. Vejamos um exemplo de um comportamento que podemos adicionar ao domínio:

Exemplo:

public void AdicionarItem(ProdutoId produtoId, Dinheiro preco, int quantidade)
{
    if (quantidade <= 0)
        throw new ExcecaoDominio("A quantidade deve ser maior que zero.");
    if (preco.EhNegativo)
        throw new ExcecaoDominio("O preço não pode ser negativo.");
    _itens.Add(new ItemPedido(produtoId, preco, quantidade));
}

Este código:

Não pode ser ignorado
Não pode ser esquecido
Não pode ser pulado

A regra vive junto com o comportamento.

Por que o FluentValidation não é suficiente ?

O FluentValidation é excelente. Mas ele valida:

Requisições (Requests)
DTOs
Formatos (Shapes)

Ele não entende de:

Ciclo de vida da entidade
Invariantes
Transições de estado
Consistência do Agregado


Use-o — mas não o confunda com validação de domínio.

O custo de ignorar a validação de domínio

A equipe olhou para trás e percebeu quantos bugs compartilhavam a mesma causa raiz:

Pedidos em estados inválidos
Descontos aplicados duas vezes
Saldos negativos
Combinações "impossíveis"


Tudo tecnicamente válido (formato), mas tudo inválido para o domínio (regra).

A Clean Architecture tornou isso visível

Antes da Clean Architecture, a validação estava espalhada: Controllers, Services, Repositories, Helpers

Com a Clean Architecture, a pergunta ficou clara: “Aonde esta regra pertence?

Se a resposta não fosse o Domínio, estaria errada.

O anti-padrão que todo mundo escreve

Vamos dar nome aos bois:  espalhar verificações lógicas simples pelos Controllers.

if (requisicao.Total < 0)
{
    return BadRequest("Total inválido");
}

Isso não é validação. Isso é controle de danos.

Os problemas dessa abordagem são:

Lógica Descentralizada: Se você tiver 5 lugares no sistema que criam ou alteram o total (API, integração com pagamento, admin, importação de Excel), você terá que repetir esse if em todos eles.

Fragilidade: Se um desenvolvedor esquecer esse if em um novo Controller, o banco de dados aceitará um valor negativo. O sistema é "fraco" porque a segurança depende da memória do programador em cada tela, e não da estrutura do código.

Agora o fluxo correto (isso é importante)

1- O Controller recebe a requisição.
2- A Camada de Aplicação orquestra.
3- O Domínio aplica as regras.
4- A Infraestrutura persiste o estado.

Se o passo 3 for fraco — todo o sistema é fraco.

Uma exceção de domínio (feita intencionalmente)

public sealed class ExcecaoDominio : Exception
{
    public ExcecaoDominio(string mensagem)
        : base(mensagem)
    {
    }
}

Isso não é um erro técnico. É o domínio se protegendo. Dentro do domínio, exceções representam violações de regras; elas interrompem o estado inválido imediatamente e servem como sinais de negócio, não falhas de sistema.

A mudança emocional

Após implementar a validação de domínio, a equipe notou algo estranho: eles pararam de temer casos extremos. Porque o domínio:

Rejeitava operações inválidas
Explicava o porquê
Mantinha-se consistente
O sistema tornou-se previsível

Na segunda parte do artigo veremos a validação nos aggregates, Value Objects reforçando as regras, evitando modelo anêmicos, e outros recursos.

E estamos conversados...

"A esperança dos justos é alegria, mas a expectação dos perversos perecerá."
Provérbios 10:28

Referências:


José Carlos Macoratti