Home > Blog > Desenvolvimento Web
Desenvolvimento Web
JavaScript
Programação

Aggregates no Node.js: invariantes e transações

Atualizado em: 14 de outubro de 2026

Estação de trabalho usada no desenvolvimento de aplicações Node.js

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 = $3

Se 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.

10 melhores cursos de programação em 2026

Descubra os melhores cursos de programação. Aprenda a escolher o curso ideal para iniciar ou avançar na carreira de desenvolvedor

POSTS RELACIONADOS

Ver todos

Seta para a direita