Pular para o conteúdo
.NETAgregados e invariantes
Módulo 07Domain-Driven Design (DDD)

Agregados e invariantes

avancado 40 min de leitura·Atualizado · .NET 9
Resumo

Um 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:

  1. 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á set público nos internos.
  2. 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.
  3. Entre agregados, referencie por Id. Um Pedido guarda ProdutoId, não o objeto Produto. 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:

C#
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#

C#
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çaEvite
Expor coleções como IReadOnlyList e mudar via métodos da raizpublic List<Item> Itens { get; set; } (qualquer um muta)
Referenciar outros agregados por IdNavegar objeto→objeto entre agregados
Manter o agregado pequeno"Deus-agregado" que carrega meio banco
Atenção

modele 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/rowversion no EF Core) para detectar escritas simultâneas na mesma raiz.

8. Exercícios#

  • Fácil: adicione ao Pedido uma invariante "máximo de 20 itens".
  • Médio: torne ItemPedido imutável e faça a alteração de quantidade via método da raiz.
  • Desafio: justifique por que Produto e Pedido devem 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#

44-03 — Agregados e invariantes | Curso ASP.NET Core