A encomenda lacrada
Uma encomenda é lacrada e etiquetada como uma unidade: você não abre a caixa para trocar um item lá dentro no meio do transporte — você lida com a encomenda inteira, pela etiqueta. Se precisar mudar o conteúdo, a transportadora abre, ajusta e lacra de novo, garantindo que peso e conteúdo continuam batendo com a etiqueta. O agregado é essa encomenda lacrada: a raiz é a etiqueta, o lacre é a regra de que ninguém mexe nas partes por fora, e a consistência (peso × conteúdo) é verificada a cada vez que a caixa é fechada.
A fronteira é a decisão
O difícil no agregado não é a raiz nem as regras de acesso — é onde traçar a fronteira. Ela responde a uma pergunta de negócio, não técnica: o que precisa estar consistente no mesmo instante (dentro do agregado) e o que pode ficar consistente daqui a pouco (entre agregados).
Errar para grande é o mais comum: um agregado que engloba objetos demais vira um ponto único de contenção, porque toda operação que toca qualquer parte precisa bloquear o todo. Errar para pequeno deixa uma invariante real sem quem a guarde.
Duas regras mantêm o agregado saudável. A primeira: referencie outros agregados por id, nunca embutindo o objeto inteiro. Um Pedido não guarda o objeto Cliente, guarda o clienteId. Isso mantém o agregado pequeno, evita carregar meio banco para abrir um pedido, e deixa claro que Cliente é outra fronteira de consistência, com sua própria transação.
A segunda: um agregado, uma transação. A consistência dentro de um agregado é imediata e forte (a raiz valida antes de salvar); a consistência entre agregados diferentes é eventual, coordenada por eventos ou por uma saga. Isso liga o agregado direto ao resto do catálogo: quando 'pagar o pedido' precisa também 'baixar o estoque' — dois agregados —, não se faz numa transação gigante que trava os dois; publica-se um evento e o estoque reage. A fronteira do agregado é, na prática, a fronteira entre o que é transação e o que é mensagem.
Como isso aparece na prática
O item alterado por fora que furou o limite
Cenário
Um sistema permitia adicionar itens a um pedido acessando a lista de itens diretamente. A regra 'pedido não passa de um valor máximo' estava no serviço, e um caminho novo de código esqueceu de checá-la, criando pedidos acima do limite.
Como o padrão entra
A lista de itens foi encapsulada dentro da raiz Pedido, e a única forma de adicionar item passou a ser pedido.adicionarItem(...), que valida a invariante do total antes de aceitar.
Trade-off: O acesso ficou menos direto — não dá mais para mexer nos itens por fora. É exatamente o ponto: a invariante passou a ter um dono único, e nenhum caminho de código consegue mais furá-la.
O agregado gigante que travava tudo
Cenário
Um agregado Cliente incluía pedidos, endereços e histórico inteiro. Qualquer mudança — até atualizar um telefone — bloqueava o cliente todo, e clientes ativos viviam em contenção sob concorrência.
Como o padrão entra
O agregado foi quebrado: Cliente, Pedido e Endereço viraram agregados separados, referenciando-se por id. Cada um passou a ser uma transação independente.
Trade-off: Operações que antes eram uma navegação direta de objetos viraram buscas por id e, às vezes, consistência eventual entre agregados. Em troca, a contenção sumiu e cada agregado passou a escalar sozinho.
Onde isto dá errado
Deixar acessar as partes por fora da raiz
Se qualquer código pode mexer num item de pedido diretamente, a raiz deixa de ser a guardiã das invariantes e a regra volta a ficar espalhada. Toda mudança tem que passar pela raiz, ou o agregado não protege nada.
Agregado grande demais
Englobar objetos que não precisam de consistência imediata entre si transforma o agregado num ponto único de contenção: toda operação bloqueia o todo. A fronteira deve conter só o que muda junto de verdade.
Embutir outros agregados em vez de referenciar por id
Guardar o objeto Cliente inteiro dentro do Pedido acopla as duas fronteiras, infla a carga e sugere uma consistência imediata que não existe. Entre agregados, referencie por id e aceite consistência eventual.
A raiz que guarda a invariante
// Agregado: um grupo de objetos tratado como uma unidade, com uma
// RAIZ que e a unica porta de entrada e a guardia das invariantes.
class Pedido { // <- raiz do agregado
private itens: Item[] = [];
private constructor(readonly id: string) {}
// Ninguem adiciona item por fora: passa pela raiz, que valida.
adicionarItem(produto: string, preco: number, qtd: number) {
if (this.total() + preco * qtd > 100_000) {
throw new Error("pedido excede o limite"); // invariante do TODO
}
this.itens.push(new Item(produto, preco, qtd));
}
total() { return this.itens.reduce((s, i) => s + i.subtotal(), 0); }
}
// Regras que valem para agregados pequenos e desacoplados:
// 1. So a raiz e referenciada de fora (Item nunca vaza sozinho).
// 2. Outros agregados sao referenciados por ID, nao por objeto.
// 3. Um agregado = uma transacao = uma fronteira de consistencia.Quando usar × quando evitar
Use quando
3- Quando há invariantes envolvendo mais de um objeto que precisam valer sempre.
- Para definir a fronteira de uma transação.
- Ao separar o que precisa ser consistente agora do que pode ser depois.
Evite quando
2- Em modelos CRUD sem invariantes entre entidades.
- Quando a fronteira ficaria grande a ponto de tudo disputar o mesmo agregado.
Testar o que ficou
3 armadilhas de Agregado viram perguntas, com o nome do padrão escondido, misturadas com padrões parecidos.
Onde isto aparece de verdade
Você já usa isto, talvez sem saber o nome.
Pedido que controla seus ItensO pedido é a raiz do agregado: você não altera um item por fora, mexe tudo pela raiz, que guarda a invariante do total.a fronteira de uma transaçãoUm agregado costuma ser a unidade que se salva e se bloqueia junto — a fronteira natural de consistência de uma transação.referência a outro agregado por idApontar para outro agregado por id, e não por objeto embutido, é a regra que mantém os agregados pequenos e desacoplados.
O padrão em uma linha
Sem nome de domínio, sem cerimônia. Só a ideia.
// Fronteira de consistência: muda o agregado pela raiz.
pedido.adicionarItem(sku, 2);
await repo.salvar(pedido);O que ele cobra
1 salto a mais entre a chamada e o efeito.
- Definir a fronteira certa é difícil, e um agregado grande demais vira gargalo de contenção
- Referenciar outros agregados só por id espalha a lógica que antes seria uma navegação direta de objetos
Não vale se: o modelo é um CRUD sem invariantes entre entidades — sem regra que atravessa objetos, a fronteira de agregado não protege nada.
Onde isto aparece
Continue por aqui
- Value ObjectNem tudo no domínio tem identidade. Dez reais é dez reais; um CEP é um CEP — não existe 'este' e 'aquele', existe o valor. Um Value Object é um tipo definido só pelo seu conteúdo, imutável e com igualdade por valor, que carrega as regras daquele conceito em vez de deixá-las espalhadas sobre um number ou string cru. É o antídoto da obsessão por primitivos.
- RepositoryUma coleção de objetos de domínio que finge não ter banco atrás. O repositório expõe operações na linguagem do negócio — 'pedidos pendentes há mais de uma hora' — e é o único lugar que sabe como isso vira consulta.
- Níveis de isolamentoTransações concorrentes se atrapalham de formas específicas e nomeadas. O nível de isolamento é a escolha de quais dessas anomalias o banco vai impedir — e cada degrau a mais custa concorrência, ou custa transações abortadas.