Aggregates no Node.js definem limites de consistência dentro do domínio. Um aggregate reúne entidades e Value Objects que precisam obedecer regras em conjunto, enquanto uma raiz controla qualquer alteração realizada no grupo.
O padrão evita que controllers, repositories e jobs modifiquem partes internas sem respeitar invariantes. Também ajuda a decidir o que carregar, salvar e proteger em uma única transação.
O que é um aggregate?
Um aggregate é um conjunto de objetos tratado como uma unidade. Em um pedido, a raiz pode ser Pedido, com itens internos. Código externo não deve alterar diretamente um item; ele solicita a mudança à raiz.
export class Pedido {
#itens = [];
constructor({ id, clienteId, status = 'rascunho', itens = [] }) {
this.id = id;
this.clienteId = clienteId;
this.status = status;
this.#itens = [...itens];
}
adicionarItem({ sku, quantidade, preco }) {
if (this.status !== 'rascunho') {
throw new Error('Pedido fechado para alterações');
}
if (quantidade <= 0) throw new Error('Quantidade inválida');
this.#itens.push({ sku, quantidade, preco });
}
get itens() {
return this.#itens.map(item => ({ ...item }));
}
}Raiz do aggregate
A raiz é a única referência usada por objetos externos. Ela protege regras como:
- pedido deve ter pelo menos um item;
- total não pode ser negativo;
- pedido pago não pode ser editado;
- cupom só pode ser aplicado uma vez;
- transição de status deve ser válida.
Essa centralização reduz combinações impossíveis.
Limite transacional
Uma alteração em um aggregate deve ser persistida atomicamente. Se a confirmação do pedido altera status, total e eventos pendentes, esses dados devem ser gravados na mesma transação.
Outros aggregates são atualizados separadamente, geralmente por eventos. Evite uma transação que bloqueia pedido, estoque, pagamento e entrega ao mesmo tempo.
Referências por ID
Um aggregate deve referenciar outro pelo identificador, não carregar todo o grafo.
const pedido = new Pedido({
id: 'p-1',
clienteId: 'c-9'
});O pedido não precisa conter uma entidade completa de cliente. Isso reduz acoplamento e tamanho.
Como identificar o limite?
Pergunte quais regras precisam ser verdade imediatamente após uma operação. Se a regra tolera consistência eventual, os objetos podem estar em aggregates diferentes.
Exemplo: o total do pedido precisa corresponder aos itens na mesma transação. Já a atualização de pontos de fidelidade pode ocorrer depois.
Aggregate pequeno
Aggregates grandes causam carregamento excessivo, conflitos de concorrência e locks longos. Prefira limites mínimos que preservam invariantes.
Um catálogo inteiro não deve ser um aggregate. Cada produto pode ser independente, e coleções podem ser consultadas por projeções.
Repository por raiz
Repositories normalmente são definidos para raízes de aggregate.
export function criarPedidoRepository(pool) {
return {
async obterPorId(id) {
const pedidoRow = await pool.query(
'SELECT * FROM pedidos WHERE id = $1', [id]
);
const itens = await pool.query(
'SELECT * FROM pedido_itens WHERE pedido_id = $1', [id]
);
return mapearPedido(pedidoRow.rows[0], itens.rows);
},
async salvar(pedido, client = pool) {
// atualiza raiz e itens dentro da mesma transação
}
};
}Veja Repository Pattern no Node.js.
Concorrência otimista
Duas requisições podem carregar a mesma versão e tentar salvar alterações. Use um campo de versão.
UPDATE pedidos
SET status = $1, version = version + 1
WHERE id = $2 AND version = $3Se nenhuma linha for alterada, houve conflito. Recarregue ou retorne erro apropriado.
Eventos do aggregate
A raiz pode registrar fatos durante a operação.
confirmar() {
if (this.#itens.length === 0) throw new Error('Pedido vazio');
this.status = 'confirmado';
this.#eventos.push({
type: 'PedidoConfirmado',
pedidoId: this.id,
version: this.version + 1
});
}Os eventos são persistidos ou enviados depois que a alteração é confirmada.
Criação por fábrica
Se a criação envolve regras complexas, use uma fábrica.
export function criarPedido({ idGenerator, clock }) {
return Pedido.novo({
id: idGenerator.next(),
criadoEm: clock.now()
});
}A fábrica coordena dependências sem tornar a entidade acoplada ao ambiente.
Hidratação
Separar criação de hidratação evita emitir eventos durante a leitura do banco.
Pedido.hidratar({ id, status, itens, version });
Pedido.novo({ id, clienteId, criadoEm });Deletes e coleções
Ao remover um item, a raiz deve validar o resultado. Não exponha o array interno para uso de splice() fora do aggregate.
Consultas
Nem toda leitura precisa hidratar aggregates. Telas e relatórios podem usar queries específicas e DTOs. O aggregate é necessário quando uma regra será executada.
Integração com HTTP
Controllers carregam a entrada e chamam um caso de uso. Eles não manipulam internals.
const pedido = await confirmarPedido({ pedidoId: req.params.id });
res.json({ id: pedido.id, status: pedido.status });Testes
const pedido = Pedido.novo({ id: 'p1', clienteId: 'c1' });
pedido.adicionarItem({ sku: 'A', quantidade: 1, preco: 1000 });
pedido.confirmar();
assert.equal(pedido.status, 'confirmado');
assert.throws(() => pedido.adicionarItem({ sku: 'B', quantidade: 1, preco: 500 }));Aggregate e banco documental
Documentos podem armazenar o aggregate em um único registro, mas o limite não deve ser decidido apenas pelo formato do banco. Considere tamanho, concorrência e regras.
Erros comuns
- um aggregate para todo o sistema;
- referências diretas entre aggregates;
- repository para cada entidade interna;
- alterações fora da raiz;
- carregar aggregate para relatórios;
- ignorar concorrência;
- publicar eventos antes do commit;
- confundir relacionamento do banco com limite de domínio.
Checklist
- há uma raiz clara;
- invariantes ficam protegidas;
- transação cobre apenas um aggregate;
- outros aggregates são referenciados por ID;
- o limite é pequeno;
- concorrência é tratada;
- consultas não dependem do modelo de escrita.
Conclusão
Aggregates no Node.js ajudam a decidir onde a consistência precisa ser imediata e quem pode alterar cada parte do modelo. A raiz protege invariantes, enquanto repositories e transações persistem a unidade.
Comece pequeno, modele regras reais e use eventos para coordenar limites diferentes. Leia também Domain-Driven Design no Node.js e Entidades e Value Objects no Node.js.
Consulte a referência de DDD e o material de Martin Fowler sobre Aggregates.

