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

Domain-Driven Design no Node.js: guia prático

Atualizado em: 14 de outubro de 2026

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

Domain-Driven Design no Node.js é uma abordagem para desenvolver sistemas complexos a partir do conhecimento do negócio. Em vez de organizar toda a aplicação ao redor de controllers, tabelas ou frameworks, o DDD coloca conceitos, regras e linguagem do domínio no centro do código.

Node.js combina bem com essa abordagem porque módulos, funções, classes e composição explícita permitem separar domínio, aplicação e infraestrutura sem exigir um framework específico. O ponto principal não é criar muitas pastas, mas representar corretamente regras que mudam, decisões importantes e limites entre partes do sistema.

Neste guia, você entenderá linguagem ubíqua, bounded contexts, entidades, value objects, aggregates, serviços de domínio, eventos e integração com bancos, APIs e filas.

O que é domínio?

Domínio é a área de conhecimento que o software atende. Em uma plataforma de pagamentos, conceitos como cobrança, autorização, estorno, conciliação e antifraude fazem parte do domínio. Em um e-commerce, catálogo, preço, carrinho, pedido, estoque e entrega formam áreas diferentes.

O software deve usar termos que especialistas e desenvolvedores reconhecem. Nomes genéricos como processData() e handleItem() escondem intenção. Prefira autorizarPagamento(), reservarEstoque() e cancelarPedido().

Linguagem ubíqua

A linguagem ubíqua é o vocabulário compartilhado entre negócio e equipe técnica. Ela aparece em reuniões, documentação, testes e código.

export class Pedido {
  confirmarPagamento(pagamento) {
    if (!pagamento.aprovado) {
      throw new Error('Pagamento não aprovado');
    }
    this.status = 'pago';
  }
}

O método expressa uma ação do negócio. Evite traduzir conceitos importantes para nomes técnicos vagos.

Bounded Context

Um bounded context define o limite no qual um modelo e sua linguagem são válidos. A palavra “cliente” pode representar o comprador no contexto de vendas, o titular no contexto de cobrança e o destinatário no contexto de entrega.

Separar contextos evita um modelo universal com dezenas de campos opcionais. Cada contexto possui regras, dados e integrações próprias.

Context Map

O mapa de contextos documenta como áreas se relacionam. Exemplos:

  • Pedidos consulta preços do Catálogo;
  • Pagamentos informa aprovação ao contexto de Pedidos;
  • Estoque recebe uma solicitação de reserva;
  • Entrega consome o endereço confirmado.

A comunicação pode ocorrer por API, eventos, arquivos ou integração direta. A escolha deve considerar consistência, autonomia e operação.

Entidades

Entidades possuem identidade contínua. Um pedido continua sendo o mesmo pedido após mudar de status ou endereço.

export class Pedido {
  constructor({ id, clienteId, itens = [], status = 'rascunho' }) {
    this.id = id;
    this.clienteId = clienteId;
    this.itens = itens;
    this.status = status;
  }

  adicionarItem(item) {
    if (this.status !== 'rascunho') {
      throw new Error('Pedido não pode mais ser alterado');
    }
    this.itens.push(item);
  }
}

A entidade protege invariantes, em vez de depender de validações espalhadas nos controllers.

Value Objects

Value Objects são definidos pelos valores, não por identidade. Dinheiro, endereço, período e e-mail são exemplos comuns.

export class Dinheiro {
  constructor(valor, moeda = 'BRL') {
    if (!Number.isInteger(valor) || valor < 0) {
      throw new Error('Valor deve ser inteiro em centavos');
    }
    this.valor = valor;
    this.moeda = moeda;
    Object.freeze(this);
  }

  somar(outro) {
    if (outro.moeda !== this.moeda) throw new Error('Moedas diferentes');
    return new Dinheiro(this.valor + outro.valor, this.moeda);
  }
}

Imutabilidade reduz estados intermediários e facilita testes.

Aggregates

Um aggregate reúne objetos que precisam manter consistência juntos. A raiz do aggregate controla alterações internas. Em um pedido, a raiz pode impedir itens inválidos, recalcular total e controlar transições de status.

Aggregates devem ser pequenos. Carregar um grafo enorme em toda operação aumenta contenção, memória e complexidade.

Serviços de domínio

Uma regra pode envolver vários objetos sem pertencer naturalmente a uma entidade. Nesse caso, use um serviço de domínio.

export function calcularFrete({ pedido, tabela, destino }) {
  const peso = pedido.pesoTotal();
  return tabela.calcular({ peso, destino });
}

Serviços de domínio não devem virar uma pasta genérica para qualquer função. Eles representam decisões do negócio.

Serviços de aplicação

A camada de aplicação coordena casos de uso: carrega aggregates, chama regras, persiste e publica eventos.

export function criarConfirmarPedido({ pedidos, pagamentos, eventos }) {
  return async function confirmar({ pedidoId, pagamentoId }) {
    const pedido = await pedidos.obterPorId(pedidoId);
    const pagamento = await pagamentos.obterPorId(pagamentoId);
    pedido.confirmarPagamento(pagamento);
    await pedidos.salvar(pedido);
    await eventos.publicar(...pedido.removerEventos());
    return pedido;
  };
}

Veja também Service Layer no Node.js.

Repositories

Repositories oferecem uma coleção orientada ao domínio. O contrato deve falar em aggregates e consultas relevantes, não expor detalhes de SQL.

export class PedidoRepository {
  async obterPorId(id) {}
  async salvar(pedido) {}
}

A implementação pode usar PostgreSQL, MongoDB ou memória. Consulte Repository Pattern no Node.js.

Eventos de domínio

Eventos registram fatos relevantes que já aconteceram: PedidoConfirmado, PagamentoAprovado ou EstoqueReservado.

pedido.adicionarEvento({
  type: 'PedidoConfirmado',
  pedidoId: pedido.id,
  occurredAt: new Date().toISOString()
});

Eventos podem acionar integrações, auditoria e processos assíncronos. A publicação confiável exige padrões como Outbox.

DDD e arquitetura

DDD estratégico define contextos e relações. DDD tático oferece padrões para modelar o interior. Ele pode ser combinado com Clean Architecture no Node.js e Arquitetura Hexagonal no Node.js.

Estrutura de pastas

src/
  pedidos/
    domain/
      pedido.js
      dinheiro.js
      events/
    application/
      confirmar-pedido.js
    infrastructure/
      postgres-pedido-repository.js
      http-controller.js
  main.js

A estrutura deve refletir contextos e capacidades. Não existe modelo obrigatório.

Validação

Validação de formato na borda protege o sistema contra JSON inválido. Invariantes do negócio ficam no domínio. Um schema pode validar que quantidade é número; o aggregate decide se a quantidade é permitida naquele estado.

Transações

Uma transação normalmente protege a gravação de um aggregate e seus registros auxiliares. Evite transações distribuídas entre contextos. Para processos longos, prefira eventos, idempotência e compensações.

Testes

Testes de domínio devem ser rápidos e não depender de rede ou banco.

const pedido = new Pedido({ id: 'p1', clienteId: 'c1' });
pedido.adicionarItem({ sku: 'A', quantidade: 2 });
assert.equal(pedido.itens.length, 1);

Repositories e adapters precisam de testes de integração separados.

Quando usar DDD

DDD é útil quando há regras complexas, múltiplas equipes, vocabulário específico e mudanças frequentes. Em um CRUD simples, modelagem direta pode ser suficiente.

Erros comuns

  • usar DDD apenas como estrutura de pastas;
  • criar classes sem comportamento;
  • colocar toda lógica em services;
  • usar um modelo único para todos os contextos;
  • criar abstrações antes de entender o negócio;
  • publicar eventos sem garantia transacional;
  • confundir entidade de domínio com linha do banco;
  • ignorar especialistas do domínio.

Fluxo recomendado

  1. identifique capacidades e termos;
  2. mapeie contextos;
  3. escolha um caso de uso importante;
  4. modele invariantes;
  5. defina portas de persistência e integração;
  6. escreva testes de domínio;
  7. implemente adapters;
  8. observe e refine a linguagem.

Conclusão

Domain-Driven Design no Node.js ajuda a transformar conhecimento do negócio em código explícito, testável e independente de detalhes técnicos. A abordagem funciona melhor quando a equipe investe em linguagem, limites e colaboração, não apenas em padrões.

Comece pelo problema mais importante, mantenha aggregates pequenos e deixe frameworks na borda. O modelo deve facilitar decisões do negócio e evoluir junto com o entendimento da equipe.

Consulte a introdução oficial de Eric Evans ao DDD e o resumo de Domain-Driven Design de Martin Fowler.

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