Agregados Raiz e Especificações
Padrões Estruturais para um Domínio Robusto
Introdução aos Conceitos de Domínio
Ao desenvolver sistemas complexos seguindo os princípios do Domain-Driven Design (DDD), surgem necessidades específicas de organização das entidades do domínio e das regras de consulta. Dois padrões fundamentais que abordaremos são o Aggregate Root (Agregado Raiz) e o Specification Pattern (Padrão de Especificação).
Trabalhando com Diferentes Entidades
Toda vez que lidamos com diferentes entidades em uma aplicação seguindo Domain-Driven Design, precisamos organizar essas entidades de forma estruturada. Vamos criar uma nova entidade chamada Category para exemplificar os conceitos.
No projeto Domain, dentro do diretório Entities, criamos:
using CleanArchitectureStore.Domain.Abstractions;
namespace CleanArchitectureStore.Domain.Entities;
public class Category
{
// Inicialmente criamos uma classe vazia para exemplificação
}Agregados Raiz
Compreendendo Agregados e Agregados Raiz no DDD
Ao olharmos para Domain-Driven Design, encontramos dois conceitos fundamentais: agregado e agregado raiz.
No contexto de uma loja, temos que entender:
Produto é o agregado (Aggregate)
Categoria seria o agregado raiz (AggregateRoot)
O que são Agregados e Agregados Raiz?
Um agregado é um conjunto de objetos de domínio que são tratados como uma unidade para propósito de mudanças de dados. O agregado raiz é a entidade principal desse agrupamento, atuando como ponto de acesso único.
Por que essa distinção é importante? No contexto de uma loja, faz sentido ter um repositório apenas para Produto e "anexar" a categoria a ele para persistência. Não faz sentido ter um repositório de categoria neste contexto específico, embora possa fazer sentido ter um repositório de categoria em um módulo de BackOffice para manutenção de categorias e outros itens que compõem a lógica.
Exemplo Prático:
Quando você cria um produto, você associa uma categoria existente
A categoria não é criada ou modificada através do produto
A categoria tem seu próprio ciclo de vida e regras
Tudo é persistido através do agregado raiz
O Problema da Persistência Indiscriminada
Em sistemas que não seguem padrões de arquitetura claros, é comum observar a criação de repositórios para todas as entidades do domínio. Esta abordagem apresenta vários problemas:
Duplicação de lógica de persistência
Dificuldade em manter a consistência transacional
Violação do encapsulamento do domínio
Acoplamento direto entre aplicação e estrutura de dados
Implementando a Interface IAggregateRoot
Para classificar as entidades como agregados e agregados raiz, geralmente cria-se uma interface chamada IAggregateRoot dentro de Abstractions no projeto Domain que não tem nada dentro:
Esta interface não contém membros porque seu único propósito é servir como um marcador para identificar quais entidades são agregados raiz.
Elegendo Product como Agregado Raiz
Pode-se então ir até uma entidade, por exemplo, Product, e elegê-la como agregado raiz implementando IAggregateRoot:
Esta é apenas uma forma de classificar as entidades como agregados e agregados raiz.
Restringindo Repositórios Apenas para Agregados Raiz
Uma das coisas importantes para trabalhar o RepositoryPattern junto do Aggregate e AggregateRoot é restringir na abstração dos repositórios em IRepository existente dentro do diretório Repositories ainda dentro do projeto Domain. Vamos substituir Entity por IAggregateRoot:
Antes:
Depois:
Com isso, permitimos que sejam criados repositórios somente para Agregados Raiz e não permitimos que sejam criados para outros agregados ou mesmo para entidades normais.
Por que essa restrição é importante?
Não queremos ter a possibilidade de ter repositórios para todas as entidades do domínio porque muitas vezes não faz sentido fazer operações CRUD em cima de qualquer entidade do domínio. Então queremos eleger determinadas entidades para serem persistidas e lidas do banco, e assim por diante.
Testando a Restrição
Se no domínio Product for retirado o IAggregateRoot inserido anteriormente, e com a restrição inserida no IRepository para restringir IAggregateRoot ao invés de Entity, a interface de IProductRepository vai falhar, porque o Repositório de Produto não é mais um agregado raiz. Agora só é possível criar repositórios de agregados raiz.
De:
para:
... isso quebra a compilação.
O mesmo vai acontecer se for tentado criar um repositório de uma entidade que não for um agregado raiz, tal como a categoria.
Benefícios Desta Abordagem
Consistência garantida: Todas as mudanças no agregado passam pelo Aggregate Root
Redução de acoplamento: A aplicação interage apenas com agregados raiz
Melhor desempenho transacional: Operações relacionadas são agrupadas naturalmente
Clareza arquitetural: A estrutura do código reflete a estrutura do domínio
Removendo a Classe Category de Exemplo
Vamos remover a classe Category utilizada para fins de exemplificação até o momento (apenas didática), pois não vamos precisar dela:
Esta é uma abordagem muito usada juntamente com Domain Driven Design.
Specification Pattern
Introdução ao Specification Pattern para Consultas
O próximo tópico que vamos passar a explorar agora é o Specification Pattern para criar especificações de leitura.
Distinção entre Regras de Gravação e Leitura
Regras de gravação não passarão pelo repositório e provavelmente serão feitas pela camada de aplicação
Regras de leitura sim, passarão pelo repositório
Exemplo Prático: Se a entidade produto tivesse uma propriedade booleana IsActive e quisesse fazer uma especificação para garantir que a leitura dos produtos, a consulta no banco de dados, não trará nenhum produto inativo, como fazer isso? Como testar isso?
A resposta é simples: criar uma especificação. Criamos uma especificação, testamos ela, e depois só a reusamos dentro das queries e consultas.
Criando a Interface ISpecification
Para isso, cria-se uma interface genérica em Abstractions do projeto Domain chamada ISpecification com apenas 2 métodos:
Entendendo os Componentes:
ToExpression(): Espera-se uma expressão que representa a consulta que desejamos executar. É uma expressão genérica Func<T, bool> que recebe um objeto do tipo T e retorna um booleano.
IsSatisfiedBy(T entity): Espera-se uma entidade que é satisfeita por um booleano. Este método verifica se uma entidade específica atende aos critérios da especificação.
Criando a Classe Base Specification
Também cria-se uma classe abstrata genérica base em Abstractions do projeto Domain chamada Specification.cs que implementa a interface ISpecification<T> criada acima:
Como funciona IsSatisfiedBy?
IsSatisfiedBy?Chama
ToExpression()para obter a árvore de expressãoChama
.Compile()para converter a expressão em uma função executávelExecuta a função passando a entidade como parâmetro
Retorna o resultado booleano
Esta é uma "receita de bolo" para criarmos nossos specifications de forma padronizada.
Entendendo Expression, Func e Delegates
Criando uma Especificação Concreta
Precisamos de uma explicação bem ampla num tópico à parte sobre Expression, Func, etc., mas por enquanto, vamos criar o diretório Specifications/Products no projeto Domain e a classe GetProductByIdSpecification, que seria a especificação para obter o produto pelo Id.
Neste caso será recebido um Guid id, a classe será um ISpecification<T> onde T é Product. Vamos sobrescrever o método Expression com filtro para o Id do produto:
O que está acontecendo aqui?
Ao invés de chapar essa consulta no repositório, criamos uma especificação para ela. Consegue-se testar essa especificação e usar ou reusar ela em diferentes repositórios.
Aqui só é definida a expressão de filtro. Note que usamos uma expressão lambda product => product.Id == _id que será convertida pelo Entity Framework para uma cláusula WHERE no SQL.
Modificando a Interface do Repositório
Agora vamos para os repositórios ainda no projeto Domain. Temos o IProductRepository e ao invés de esperar um Guid id será esperado um Specification<Product> chamado specification:
Antes:
Depois:
Consequentemente, isso quebrará o repositório na infraestrutura e o handler na aplicação, então precisamos atualizar ambos.
Atualizando a Implementação do Repositório
Começando pelo ProductRepository na infraestrutura. Novamente, ao invés de esperar um Guid id, esperar agora um Specification<Product> chamado specification, e consequentemente no FirstOrDefaultAsync não é possível mais utilizar x.Id == id sendo necessário trocar por um Where com a expressão da especificação convertendo para uma expressão lambda:
Antes:
Depois:
Essa seria a modificação do ProductRepository com a utilização de specification.ToExpression() ao invés de utilizar o id direto.
Explicação das Mudanças:
AsNoTracking(): Adicionado para melhor performance. O Entity Framework não rastreia as entidades retornadas, o que é ideal para operações apenas de leitura.Where(specification.ToExpression()): Aplica a expressão da especificação como filtro na consulta.Remoção do filtro direto: Não usamos mais
x => x.Id == iddiretamente.
Atualizando o Handler na Camada de Application
Agora nosso Handler no projeto Application precisa alterar request.id para passar a utilizar uma expressão. Vamos criar um var spec que será uma instância de GetProductByIdSpecification passando o request.Id para ele, e alterar no repository.GetByIdAsync(spec) para passar a utilizá-lo:
Antes:
Depois:
Importante: Como estamos usando Command nada quebra na nossa API. A mudança é interna e transparente para os consumidores da API.
Testando a Implementação Completa
Vamos rodar a aplicação novamente, lembrando de executar o banco de dados:
Testando no Postman:
Exemplos Adicionais de Especificações
Para um entendimento mais completo, vejamos mais exemplos de especificações:
O Problema das Regras de Consulta Espalhadas
Quando as regras de consulta estão espalhadas por diferentes camadas (repositórios, serviços, handlers), encontramos:
Dificuldade de reutilização de critérios
Complexidade nos testes unitários
Duplicação de lógica de filtragem
Dificuldade em combinar múltiplos critérios
Solução: Especificações como Cidadãs de Primeira Classe
O Specification Pattern encapsula critérios de consulta em objetos que podem ser combinados, reutilizados e testados independentemente.
Benefícios Combinados
Ao utilizar Aggregate Roots e Specifications em conjunto, obtemos:
Domínio rico e expressivo que encapsula comportamento
Separação clara de responsabilidades entre camadas
Testabilidade aprimorada com especificações isoladas
Flexibilidade de consultas sem vazar detalhes de infraestrutura
Mantenabilidade através de código organizado por responsabilidade
Testando as Especificações
Testes Unitários para Especificações
Boas Práticas e Considerações Finais
Quando Usar Aggregates Roots
Entidades com ciclo de vida independente
Agrupamentos que exigem consistência transacional
Raízes de hierarquias de objetos complexas
Pontos de entrada naturais para operações de negócio
Quando Usar Specifications
Critérios de consulta reutilizáveis em múltiplos lugares
Regras complexas que necessitam de teste unitário
Consultas que podem ser combinadas dinamicamente
Separação clara entre regras de negócio e implementação de persistência
Conclusão
A adoção de Aggregate Roots e Specifications representa um salto de maturidade na aplicação dos princípios do Domain-Driven Design. Estes padrões não apenas organizam o código de forma mais eficiente, mas também refletem com maior fidelidade os conceitos e regras do domínio de negócio, criando uma base sólida para a evolução sustentável do sistema.
A combinação de agregados bem definidos com especificações testáveis resulta em uma arquitetura onde as regras de negócio são explícitas, o código é previsível e as mudanças futuras podem ser implementadas com confiança, sabendo que o sistema mantém sua integridade e coerência em todas as camadas.
Seguindo o fluxo das anotações originais, implementamos:
A criação da entidade Category para exemplificação
A compreensão de agregados e agregados raiz no DDD
A implementação da interface
IAggregateRootA restrição de repositórios apenas para agregados raiz
A introdução ao Specification Pattern
A criação da interface
ISpecificatione classe baseSpecificationA implementação de uma especificação concreta
GetProductByIdSpecificationA atualização do repositório para usar especificações
A atualização do handler para criar e usar especificações
O teste completo da implementação
Esta abordagem segue o Princípio da Inversão de Dependência (DIP), onde detalhes de implementação (infraestrutura) dependem de abstrações definidas no domínio, resultando em um código mais testável, flexível e alinhado com as melhores práticas de arquitetura de software.
Atualizado