Pular para o conteúdo

Agregados

Para Eric Evans, o Agregado agrupa objetos associados que devem ser tratados como uma unidade para fins de consistência de dados. Ele define uma fronteira e uma raiz: a raiz é a única entidade acessível de fora; tudo dentro só é manipulado através dela.

Vaughn Vernon, no artigo Effective Aggregate Design, dá as regras práticas mais influentes: prefira agregados pequenos, referencie outros agregados por identidade e use consistência eventual entre agregados.

Critério Entidade Agregado
Escopo Objeto individual Cluster de objetos com fronteira
Acesso Direto Somente via raiz
Consistência De si mesmo De toda a fronteira (invariantes que cruzam objetos)
Transação Unidade transacional (1 agregado por transação)

Controla onde ficam as invariantes que envolvem múltiplos objetos. Ex.: “a soma dos itens não pode exceder o limite do pedido”. Essa regra atravessa Pedido e ItemPedido — logo precisa de uma raiz (Pedido) que a proteja. Sem agregados, essas regras vazam para services e ficam impossíveis de garantir.

  1. Proteja invariantes verdadeiras dentro de uma fronteira de consistência. O que precisa ser consistente imediatamente fica no mesmo agregado.
  2. Prefira agregados pequenos. Grandes causam contenção, locks e problemas de performance/memória.
  3. Referencie outros agregados por ID, nunca por referência de objeto direta. (clienteId, não Cliente cliente.)
  4. Uma transação modifica um único agregado. Consistência entre agregados é eventual, via Domain Events.

Agregar demais em nome de “conveniência” é o erro mais frequente. Se dois conceitos não compartilham invariantes imediatas, não devem estar no mesmo agregado. Em subdomínios genéricos ou consultas puras (CQRS lado leitura), a rigidez do agregado pode ser dispensada.

Raiz Pedido controlando o acesso aos itens, protegendo invariantes e referenciando o cliente por ID.

package com.loja.pedido.domain;
import com.loja.shared.domain.Dinheiro;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
public class Pedido {
private static final int MAX_ITENS = 50;
private final PedidoId id;
private final ClienteId clienteId;
private final List<ItemPedido> itens;
private final Dinheiro limite;
private StatusPedido status;
public Pedido(ClienteId clienteId, Dinheiro limite) {
this.id = PedidoId.novo();
this.clienteId = Objects.requireNonNull(clienteId);
this.limite = Objects.requireNonNull(limite);
this.itens = new ArrayList<>();
this.status = StatusPedido.ABERTO;
}
public void adicionarItem(ProdutoId produtoId, int quantidade, Dinheiro precoUnitario) {
garantirAberto();
if (itens.size() >= MAX_ITENS) {
throw new IllegalStateException("Limite de itens atingido");
}
ItemPedido novo = new ItemPedido(produtoId, quantidade, precoUnitario);
Dinheiro totalProjetado = total().somar(novo.subtotal());
if (totalProjetado.maiorQue(limite)) {
throw new IllegalStateException("Total excede o limite do pedido");
}
itens.add(novo);
}
public void removerItem(ProdutoId produtoId) {
garantirAberto();
itens.removeIf(item -> item.produtoId().equals(produtoId));
}
public void confirmar() {
garantirAberto();
if (itens.isEmpty()) {
throw new IllegalStateException("Pedido sem itens");
}
this.status = StatusPedido.CONFIRMADO;
}
public Dinheiro total() {
return itens.stream()
.map(ItemPedido::subtotal)
.reduce(Dinheiro.reais("0"), Dinheiro::somar);
}
private void garantirAberto() {
if (status != StatusPedido.ABERTO) {
throw new IllegalStateException("Pedido nao esta aberto");
}
}
public List<ItemPedido> itens() {
return Collections.unmodifiableList(itens);
}
public PedidoId id() {
return id;
}
public ClienteId clienteId() {
return clienteId;
}
public StatusPedido status() {
return status;
}
}
package com.loja.pedido.domain;
import com.loja.shared.domain.Dinheiro;
import java.util.Objects;
public class ItemPedido {
private final ProdutoId produtoId;
private final int quantidade;
private final Dinheiro precoUnitario;
ItemPedido(ProdutoId produtoId, int quantidade, Dinheiro precoUnitario) {
if (quantidade <= 0) {
throw new IllegalArgumentException("Quantidade deve ser positiva");
}
this.produtoId = Objects.requireNonNull(produtoId);
this.quantidade = quantidade;
this.precoUnitario = Objects.requireNonNull(precoUnitario);
}
Dinheiro subtotal() {
return precoUnitario.multiplicar(quantidade);
}
ProdutoId produtoId() {
return produtoId;
}
}
classDiagram
    class Pedido {
        <<Aggregate Root>>
        -PedidoId id
        -ClienteId clienteId
        -Dinheiro limite
        -StatusPedido status
        +adicionarItem()
        +removerItem()
        +confirmar()
        +total() Dinheiro
    }
    class ItemPedido {
        <<Value Object>>
        -ProdutoId produtoId
        -int quantidade
        -Dinheiro precoUnitario
    }
    class ClienteId {
        <<Value Object / ref>>
    }
    Pedido "1" *-- "*" ItemPedido : contém
    Pedido ..> ClienteId : referencia por ID
  • Agregados grandes demais: raiz que engloba meio domínio, gerando locks, contenção e carregamento pesado.
  • Referência direta a outro agregado: guardar Cliente cliente em vez de ClienteId, acoplando ciclos de vida.
  • Modificar dois agregados na mesma transação: viola a regra de consistência; use Domain Events para consistência eventual.
  • Acesso a membros internos por fora: manipular ItemPedido sem passar pela raiz, furando invariantes.

1. O que a Raiz do Agregado garante?

Resposta

As invariantes de toda a fronteira do agregado. É a única porta de entrada: objetos internos só são manipulados através dela, o que mantém a consistência.

2. Por que referenciar outros agregados por ID?

Resposta

Para desacoplar ciclos de vida, manter agregados pequenos, evitar carregar grafos gigantes e respeitar a regra de uma transação por agregado. Segue a recomendação de Vaughn Vernon.

3. Quantos agregados devem ser modificados por transação?

Resposta

Um. A consistência dentro do agregado é imediata; entre agregados é eventual, coordenada por Domain Events.

4. Qual o risco de agregados grandes demais?

Resposta

Contenção e locks (muitos usuários alterando a mesma raiz), carregamento pesado em memória e transações longas. A recomendação é preferir agregados pequenos.