Home Comunidade Eventos YouTube Networks Desafios // produtos Academy News CTI | CVE Contato

03 - Agregado (Aggregate) e Raiz de Agregado

#agregado#consistencia-transacional#ddd#invariantes
video explicativo
Video explicativoEspaco reservado para o video desta anotação — adicione 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.