Agregados e invariantes
ResumoUm agregado é um grupo de objetos tratado como uma unidade de consistência. A raiz do agregado é a única porta de entrada e garante as invariantes; referências externas apontam só para a raiz, por Id.
1. Objetivos de Aprendizagem#
- Entender agregado, raiz (aggregate root) e fronteira de consistência.
- Proteger invariantes dentro da raiz.
- Referenciar outros agregados por Id, não por objeto.
2. Pré-requisitos#
- Entidades e value objects (Aula 44.2).
3. Conceito#
Um agregado é um conjunto de entidades e value objects que muda junto e precisa ficar consistente como um todo. Ele tem uma raiz (uma entidade) que é a única porta de entrada: o mundo externo só fala com a raiz, nunca com os objetos internos diretamente.
Três regras práticas:
- A raiz protege as invariantes. Toda mudança passa por métodos da raiz, que garantem as regras (ex.: "um pedido não pode ter itens duplicados"). Não há
setpúblico nos internos. - Uma transação = um agregado. Você salva um agregado inteiro de forma atômica. Mudanças que cruzam agregados são coordenadas por eventos, não numa mesma transação.
- Entre agregados, referencie por
Id. UmPedidoguardaProdutoId, não o objetoProduto. Isso mantém as fronteiras nítidas e os agregados pequenos.
Manter agregados pequenos é uma diretriz central: agregados grandes viram gargalos de concorrência e de carregamento.
4. Mão na Massa#
Um Pedido é a raiz; ItemPedido é interno. Só o pedido cria/altera itens, garantindo as invariantes:
public class Pedido // <- Aggregate Root
{
private readonly List<ItemPedido> _itens = new();
public Guid Id { get; } = Guid.NewGuid();
public IReadOnlyList<ItemPedido> Itens => _itens.AsReadOnly(); // externo só lê
public Dinheiro Total => new(_itens.Sum(i => i.Subtotal.Valor), "BRL");
public void AdicionarItem(Guid produtoId, Dinheiro precoUnit, int qtd)
{
if (qtd <= 0)
throw new ArgumentException("Quantidade deve ser positiva.");
if (_itens.Any(i => i.ProdutoId == produtoId)) // INVARIANTE
throw new InvalidOperationException("Produto já está no pedido.");
_itens.Add(new ItemPedido(produtoId, precoUnit, qtd));
}
public void RemoverItem(Guid produtoId) =>
_itens.RemoveAll(i => i.ProdutoId == produtoId);
}
public class ItemPedido // <- entidade INTERNA ao agregado
{
public Guid ProdutoId { get; } // referência a OUTRO agregado: por Id
public Dinheiro PrecoUnitario { get; }
public int Quantidade { get; }
internal ItemPedido(Guid produtoId, Dinheiro precoUnit, int qtd)
{
ProdutoId = produtoId;
PrecoUnitario = precoUnit;
Quantidade = qtd;
}
public Dinheiro Subtotal =>
new(PrecoUnitario.Valor * Quantidade, PrecoUnitario.Moeda);
}
Repare: _itens é privado; o externo só vê IReadOnlyList. Ninguém adiciona item "por fora" — a invariante de não duplicar vive dentro da raiz.
5. Exemplo Completo#
var pedido = new Pedido();
pedido.AdicionarItem(produtoId: teclado.Id, precoUnit: new Dinheiro(120m), qtd: 2);
pedido.AdicionarItem(teclado.Id, new Dinheiro(120m), 1); // lança: já está no pedido
Console.WriteLine(pedido.Total);
O Pedido referencia o produto por teclado.Id — não guarda o objeto Produto. Produto e Pedido são agregados distintos, salvos em transações distintas.
6. Boas Práticas e Armadilhas#
| Faça | Evite |
|---|---|
Expor coleções como IReadOnlyList e mudar via métodos da raiz | public List<Item> Itens { get; set; } (qualquer um muta) |
Referenciar outros agregados por Id | Navegar objeto→objeto entre agregados |
| Manter o agregado pequeno | "Deus-agregado" que carrega meio banco |
Atençãomodele a fronteira pela invariante, não pela conveniência de navegação. Se duas coisas não precisam ser consistentes na mesma transação, provavelmente são agregados separados.
7. Segurança e Produção#
- Agregados pequenos reduzem contenção e conflitos de concorrência. Combine com concorrência otimista (coluna de versão/
rowversionno EF Core) para detectar escritas simultâneas na mesma raiz.
8. Exercícios#
- Fácil: adicione ao
Pedidouma invariante "máximo de 20 itens". - Médio: torne
ItemPedidoimutável e faça a alteração de quantidade via método da raiz. - Desafio: justifique por que
ProdutoePedidodevem ser agregados separados (e não um só).
9. Resumo#
O agregado é a unidade de consistência; a raiz é a única porta de entrada e guarda as invariantes; entre agregados, referencie por Id e salve um por transação. Prefira agregados pequenos.
10. Próximos Passos#
Onde as regras devem morar: domínio rico × anêmico.
11. Referências#
- Microsoft Learn — Projetando validações e agregados
- Vaughn Vernon — Effective Aggregate Design