03 - Agregado (Aggregate) e Raiz de Agregado
video: https://youtu.be/... no frontmatter da nota.Agregado (Aggregate) e Raiz de Agregado
Um agregado é um conjunto de entidades e objetos de valor que formam um todo consistente, tratado
como uma única unidade para leitura, escrita e transação. A raiz de agregado é a única entidade de
dentro que fica exposta ao exterior: todo acesso, inclusive às partes internas, acontece através
dela. O objetivo central do padrão é delimitar onde a consistência imediata é obrigatória, porque
sustentar consistência forte em todos os objetos de um sistema grande é inviável em produção. Dentro
do agregado, as invariantes valem a qualquer momento e são garantidas por transação; entre
agregados, a consistência é eventual, coordenada por eventos ou processos. Essa fronteira decide o
tamanho das transações, o contencioso de concorrência e a granularidade dos serviços, e é uma das
decisões de maior impacto no custo de operação de um sistema.
Invariantes e fronteira transacional
Uma invariante é uma regra que deve permanecer verdadeira em todos os estados possíveis do objeto,
não apenas ao final de um caso de uso. Em um pedido, a soma dos itens deve corresponder ao total, e
o total não pode ser negativo; em uma sala de cinema, dois ingressos não podem ocupar o mesmo
assento. O agregado define o menor círculo de objetos dentro do qual essas regras precisam ser
verificadas de forma atômica. Se a regra cruza a fronteira, ela passa a ser eventual, com todos os
cuidados que isso exige. A consequência de produção é direta: transações curtas de um agregado
pequeno rendem, bloqueiam menos e travam menos integrações, enquanto transações que atravessam
vários agregados geram deadlocks e timeouts sob carga. A pergunta que orienta o desenho é sempre a
mesma: qual invariante precisa ser verdadeira quando a transação termina, e nada além disso.
Acesso apenas pela raiz
Todo mundo externo ao agregado referencia a raiz e somente a raiz, e as entidades internas ficam
encapsuladas. Ninguém navega até um item do pedido e altera sua quantidade diretamente: o chamador
pede que o pedido altere o item, e o pedido valida a regra antes de executar. Esse controle central
é o que garante as invariantes, porque todos os caminhos de mutação passam por um único ponto que
as conhece. A raiz também controla o ciclo de vida das partes internas, criando e descartando
entidades internas conforme a operação exige. Na prática, essa regra define a granularidade dos
repositórios e das consultas: repositórios são por agregado, e a persistência carrega o agregado
inteiro ou nada. Deixar escapar referências internas, como devolver a lista mutável de itens, é a
quebra mais comum do padrão e anula todas as garantias construídas.
Referência por identidade entre agregados
Agregados se referenciam pelo identificador, e não por objeto direto. Um pedido guarda o id do
produto e não a entidade Produto inteira, porque carregar o produto inteiro acoplaria os dois
agregados em uma transação única e ampliaria a fronteira de consistência sem necessidade. Com a
referência por id, o pedido carrega os dados mínimos de que precisa, frequentemente uma cópia local
dos atributos usados na hora, como o nome do produto e o preço cobrado no momento da compra. Essa
cópia é uma decisão deliberada: preço e descrição mudam depois, e o histórico do pedido deve
preservar o que foi de fato acordado. O código a seguir, em Java, mostra o desenho típico.
public final class Pedido { // raiz de agregado
private final PedidoId id;
private final ClienteId clienteId; // referência por identidade a outro agregado
private final List<ItemDoPedido> itens; // entidades internas, sem exposição
private Pedido(PedidoId id, ClienteId clienteId) {
this.id = id;
this.clienteId = clienteId;
this.itens = new ArrayList<>();
}
public static Pedido abrir(PedidoId id, ClienteId clienteId) {
return new Pedido(id, clienteId);
}
public void adicionarItem(ProdutoId produto, String nomeDoProduto, Dinheiro preco, int quantidade) {
// cópia local dos atributos necessários: o histórico não deve mudar quando o produto muda
itens.add(new ItemDoPedido(produto, nomeDoProduto, preco, quantidade));
if (total().excede(new Dinheiro(new BigDecimal("20000"), Moeda.BRL))) {
throw new LimiteDoPedidoExcedido(id); // invariante verificada dentro do agregado
}
}
public Dinheiro total() {
return itens.stream().map(ItemDoPedido::subtotal)
.reduce(Dinheiro.zero(Moeda.BRL), Dinheiro::adicionar);
}
public List<ItemDoPedido> itensParaLeitura() {
return List.copyOf(itens); // cópia imutável: nunca devolver a coleção interna
}
}
Regras de desenho de agregados
A prática consolidada pode ser resumida em quatro regras, todas com motivo operacional claro.
Primeiro, proteja as invariantes verdadeiras e apenas elas: se nenhuma transação precisa verificar a
regra de uma vez, a regra não pertence ao mesmo agregado. Segundo, desenhe agregados pequenos: um
agregado com centenas de objetos carrega custo de leitura, de bloqueio e de versão otimista sem
ganho correspondente. Terceiro, modifique um agregado por transação: duas mudanças em agregados
distintos devem ser transações separadas, coordenadas por eventos, ou um processamento em etapas.
Quarto, o tamanho certo é descoberto com exemplos e casos de uso reais, conversando com o negócio
sobre o que precisa ser consistente no instante da transação. Um erro frequente é desenhar o
agregado pela estrutura das tabelas, o que reproduz o problema do modelo anêmico em outro nível.
Tamanho, concorrência e performance
O tamanho do agregado é o principal fator de contenção de escrita. Com versionamento otimista, toda
alteração incrementa a versão do agregado inteiro, então dois usuários que mexem em itens
diferentes do mesmo pedido disputam a mesma versão e um deles recebe conflito. Em um agregado de
carrinho com milhares de operações simultâneas, esse conflito se torna frequente e degrada a
experiência. A mitigação usual é reduzir o agregado: o carrinho persiste a lista de itens, e a
"reserva de estoque" vira agregado próprio com vida curta. Carregamento também pesa: trazer o
agregado inteiro para validar uma invariante é o preço da consistência, e agregados enormes tornam
leituras lentas e memoriam caches mal dimensionados. Quando a regra exige cruzar agregados, como
verificar limite de crédito do cliente a cada pedido, o padrão aceitável é validar com dados
aproximados dentro da transação e confirmar de forma assíncrona depois, assumindo a correção
eventual.
Agregados e eventos de domínio
A raiz é o ponto natural de publicação de eventos de domínio, porque é ela que sabe quando algo de
significado para o negócio aconteceu. PedidoConfirmado, ContaBloqueada e AssinaturaRenovada nascem
das transições da raiz e descrevem fatos no passado com a linguagem do negócio. O padrão de
registro é simples: a raiz acumula os eventos da transação em uma coleção interna, e a
infraestrutura despacha depois que a persistência confirma, garantindo que o evento só sai para o
mundo se o agregado de fato mudou. Esse acoplamento moderado entre agregado e evento é o mecanismo
que sustenta a consistência eventual entre agregados descrita antes, e em arquiteturas de
mensageria vira a semente da integração assíncrona entre contextos.
Sinais de agregado mal desenhado
| Sintoma em produção | Causa provável | Mitigação |
|---|---|---|
| Conflitos frequentes de versão otimista em horários de pico | Agregado grande demais com disputa de escrita | Dividir o agregado e mover a regra não essencial para fora |
| Transações com timeout e deadlocks sob carga | Uma transação atravessa vários agregados | Uma mudança de agregado por transação, com eventos |
| Consultas simples carregam dezenas de objetos desnecessários | A leitura sempre carrega o agregado completo | Reduzir o agregado e criar leituras fora do modelo |
| Invariante violada aparece no banco, não na aplicação | Caminho de mutação que não passa pela raiz | Bloquear acesso direto às entidades internas |
| Time não consegue dizer onde começa e termina um agregado | Desenho guiado pelas tabelas e não pelas invariantes | Revisar com casos de uso reais e redesenhar |
Erros comuns e boas práticas
- Desenhar um agregado gigante por contexto para "garantir consistência de tudo". Consistência
universal impossibilita operação concorrente e vira o gargalo número um do sistema. - Devolver a coleção interna mutável pela raiz. A cópia imutável custa pouco e preserva o controle
de todas as mutações. - Referenciar outros agregados por objeto direto. Referência por identificador mantém transações
pequenas e evita acoplamento de carregamento. - Validar regra de negócio entre agregados dentro da mesma transação por hábito. Prefira eventos e
consistência eventual quando a regra suporta atraso de segundos. - Ignorar o histórico em nome da simplicidade. Copiar no momento da compra os atributos que o
pedido precisa preserva o contrato acordado mesmo depois de mudanças no produto. - Trocar o desenho de agregados a cada funcionalidade sem revisão. A fronteira é decisão de
arquitetura, e mudanças nela exigem plano de migração de dados.