|
Neste artigo veremos o porque de fazer a validação no domínio da aplicação. |
“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:
Data Annotations
FluentValidation
Validação de modelo da API
Validação no Frontend
Testes de integração
E ainda assim — o impossível aconteceu. Como ????
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.
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: