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.jsA 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
- identifique capacidades e termos;
- mapeie contextos;
- escolha um caso de uso importante;
- modele invariantes;
- defina portas de persistência e integração;
- escreva testes de domínio;
- implemente adapters;
- 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.




