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

Domain-Driven Design no Node.js

Atualizado em: 3 de setembro de 2026

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

O Domain-Driven Design no Node.js, ou DDD, organiza software complexo ao redor do conhecimento do negócio. Em vez de modelar apenas tabelas, endpoints e serviços técnicos, a equipe constrói uma linguagem comum, delimita contextos e cria objetos que representam regras reais.

DDD não é uma estrutura de pastas nem exige microserviços. Ele é uma abordagem de colaboração e modelagem. Técnicas como entidades, value objects, agregados, repositories e eventos ajudam a proteger invariantes e tornar o código mais próximo da forma como especialistas descrevem o problema.

Neste guia, você aprenderá linguagem ubíqua, bounded contexts, entidades, value objects, agregados, serviços de domínio, repositories, eventos, factories, testes e integração com arquitetura hexagonal.

O que é Domain-Driven Design?

DDD foi sistematizado por Eric Evans no livro Domain-Driven Design. A abordagem é especialmente útil quando regras, exceções e processos do negócio são mais difíceis que a tecnologia usada.

O objetivo não é criar classes sofisticadas para CRUD simples. DDD concentra esforço nas partes que diferenciam o produto e exigem entendimento profundo.

Para organizar dependências, consulte Arquitetura Hexagonal no Node.js. Para casos de uso, veja Service Layer no Node.js.

Domínio e subdomínios

O domínio é a área de conhecimento do sistema. Ele pode ser dividido em:

  • Core Domain: capacidade que diferencia o negócio.
  • Supporting Subdomain: necessário, mas não diferencial.
  • Generic Subdomain: problema comum, como autenticação ou envio de e-mail.

Invista mais modelagem no core domain. Soluções prontas podem atender partes genéricas.

Linguagem ubíqua

Desenvolvedores e especialistas usam os mesmos termos em reuniões, documentação, testes e código.

Se o negócio diz “reservar estoque”, o código pode possuir:

inventory.reserve(order.items);

Evite nomes técnicos vagos como processData ou updateRecord.

Glossário vivo

Registre termos, exemplos e diferenças:

  • pedido versus carrinho;
  • cancelamento versus estorno;
  • cliente versus conta;
  • reserva versus venda;
  • pagamento autorizado versus capturado.

O glossário evolui com o modelo.

Bounded Context

Um bounded context delimita onde um modelo e uma linguagem são válidos. A palavra “produto” pode significar item comercial em catálogo e unidade física em estoque.

catalog.Product
inventory.StockItem

Não force uma única classe universal para conceitos diferentes.

Context Map

O mapa mostra relações entre contextos:

  • Customer/Supplier;
  • Conformist;
  • Anti-Corruption Layer;
  • Shared Kernel;
  • Open Host Service;
  • Published Language.

Ele ajuda a decidir contratos e responsabilidades.

Entidade

Entidade possui identidade que permanece ao longo do tempo:

export class Order {
  private constructor(
    readonly id: OrderId,
    private status: OrderStatus,
    private items: OrderItem[]
  ) {}

  addItem(product: ProductId, quantity: Quantity) {
    if (this.status !== 'draft') {
      throw new OrderAlreadySubmittedError();
    }

    this.items.push(
      OrderItem.create(product, quantity)
    );
  }
}

Duas entidades com o mesmo conteúdo ainda são diferentes se possuem IDs distintos.

Value Object

Value Object é definido pelos valores e normalmente é imutável:

export class Money {
  private constructor(
    readonly cents: number,
    readonly currency: string
  ) {}

  static create(cents: number, currency: string) {
    if (!Number.isSafeInteger(cents)) {
      throw new InvalidMoneyError();
    }

    return new Money(cents, currency);
  }

  add(other: Money): Money {
    if (other.currency !== this.currency) {
      throw new CurrencyMismatchError();
    }

    return Money.create(
      this.cents + other.cents,
      this.currency
    );
  }
}

Consulte o artigo específico sobre Value Objects desta mesma sequência quando publicado.

Agregado

Agregado é um conjunto de objetos tratados como unidade de consistência. Uma entidade é a raiz e controla alterações internas.

order.addItem(productId, quantity);
order.removeItem(productId);
order.submit();

Não altere order.items diretamente.

Aggregate Root

Somente a raiz é referenciada externamente. Outros agregados usam IDs:

class Order {
  readonly customerId: CustomerId;
}

Evite carregar o objeto Customer inteiro dentro de Order se pertencem a agregados separados.

Invariantes

Invariante é uma condição que deve permanecer verdadeira:

  • pedido enviado possui pelo menos um item;
  • quantidade é positiva;
  • total não pode ser negativo;
  • pagamento capturado não pode ser capturado novamente;
  • estoque reservado não supera disponibilidade.

A raiz protege essas regras.

Tamanho do agregado

Agregados grandes aumentam contenção e carregamento. Prefira fronteiras pequenas e consistência forte somente onde é necessária.

Consistência entre agregados

Use eventos e processos assíncronos quando a atualização imediata não é obrigatória. Pedido e envio podem evoluir em transações separadas.

Repository por agregado

export interface OrderRepository {
  findById(id: OrderId): Promise<Order | null>;
  save(order: Order): Promise<void>;
}

Repository trabalha com raízes, não com cada tabela interna. Consulte Repository Pattern no Node.js.

Factory

Uma factory encapsula criação complexa:

export class OrderFactory {
  constructor(
    private readonly ids: IdGenerator,
    private readonly clock: Clock
  ) {}

  create(input: NewOrderInput): Order {
    return Order.create({
      id: OrderId.from(this.ids.next()),
      createdAt: this.clock.now(),
      customerId: input.customerId,
      items: input.items
    });
  }
}

Criação simples pode ficar em método estático.

Serviço de domínio

Use quando uma regra não pertence naturalmente a uma entidade:

export class ShippingPriceService {
  calculate(
    order: Order,
    destination: Address,
    policy: ShippingPolicy
  ): Money {
    // regra entre conceitos
  }
}

Evite serviços de domínio que apenas chamam repositories.

Serviço de aplicação

Coordena o caso de uso:

export class SubmitOrder {
  constructor(
    private readonly orders: OrderRepository,
    private readonly uow: UnitOfWork
  ) {}

  execute(input: SubmitOrderInput) {
    return this.uow.run(async tx => {
      const order = await tx.orders.findById(
        OrderId.from(input.orderId)
      );

      if (!order) throw new OrderNotFoundError();

      order.submit();
      await tx.orders.save(order);
      await tx.outbox.addMany(order.pullEvents());
    });
  }
}

O serviço não implementa a invariante; chama a entidade.

Evento de domínio

export class OrderSubmitted {
  constructor(
    readonly orderId: string,
    readonly customerId: string,
    readonly occurredAt: Date
  ) {}
}

O evento descreve algo que aconteceu no domínio.

Registro de eventos

private events: DomainEvent[] = [];

private record(event: DomainEvent) {
  this.events.push(event);
}

pullEvents(): DomainEvent[] {
  const events = [...this.events];
  this.events = [];
  return events;
}

Outbox

Persista evento e agregado na mesma transação. Consulte Outbox Pattern no Node.js.

Domain Event versus Integration Event

Evento de domínio é interno ao modelo. Um adapter pode convertê-lo em evento de integração estável:

OrderSubmitted -> orders.order-submitted.v1

Isso evita expor detalhes internos para outros serviços.

Anti-Corruption Layer

Uma ACL traduz um sistema externo para conceitos internos:

class LegacyCustomerAdapter {
  toCustomerProfile(response: LegacyResponse) {
    return CustomerProfile.create({
      customerId: response.cod_cliente,
      riskLevel: mapRisk(response.classe)
    });
  }
}

O domínio não passa a usar os nomes do legado.

Modelo anêmico

Uma classe com apenas getters e setters, enquanto todas as regras vivem em services, perde parte do valor de DDD.

// evitar
order.status = 'approved';

Prefira:

order.approve();

Persistência e domínio

Entidades não precisam ser modelos do ORM. Um mapper converte entre representação do banco e objetos do domínio.

Reconstituição

Order.restore({
  id,
  status,
  items,
  version
});

O método de restore não deve executar regras de criação novamente.

Concorrência

Agregados podem usar versão para lock otimista:

UPDATE orders
SET status = $1,
    version = version + 1
WHERE id = $2
  AND version = $3;

Consulte Lock Otimista no Node.js.

Unit of Work

Vários repositories e outbox podem compartilhar transação. Consulte Unit of Work no Node.js.

Especificações

Uma Specification representa regra combinável:

const eligible = activeCustomer
  .and(noOverdueInvoices)
  .and(withinCreditLimit);

Use quando a regra é reutilizada e merece nome; não transforme toda condição simples em classe.

Policy

Policies representam decisões variáveis:

interface DiscountPolicy {
  calculate(order: Order, customer: Customer): Percentage;
}

Implementações podem variar por campanha ou segmento.

Módulos

src/
├── ordering/
├── billing/
├── inventory/
└── shipping/

Cada módulo pode representar um bounded context em um monólito modular.

DDD não exige microserviços

Bounded contexts podem existir no mesmo processo. Separar em serviços adiciona rede, consistência eventual e operação. Faça isso somente quando houver benefício organizacional ou técnico.

Contextos em microserviços

Quando separados, use contratos explícitos, eventos versionados e anti-corruption layers. Não compartilhe o mesmo schema de banco.

Descoberta do modelo

Use conversas com especialistas, Event Storming, exemplos e análise de decisões. O código é resultado do aprendizado, não o ponto inicial.

Event Storming

Mapeie:

  • eventos;
  • comandos;
  • atores;
  • policies;
  • sistemas externos;
  • hotspots;
  • read models.

O exercício revela linguagem e limites.

Testes de domínio

test('pedido vazio não pode ser enviado', () => {
  const order = Order.draft({ items: [] });

  assert.throws(
    () => order.submit(),
    EmptyOrderError
  );
});

Testes são rápidos e não usam banco.

Testes com exemplos do negócio

Use nomes e números reais:

test('cliente premium recebe 10% acima de 500 reais')

Exemplos fortalecem a linguagem ubíqua.

Testes de repository

Confirme que reconstituição preserva invariantes, versionamento e tipos. Use banco real para SQL e transações.

Observabilidade por contexto

Nomeie logs e métricas com linguagem do domínio, como orders.submitted e payments.captured, sem transformar IDs em labels.

Quando DDD ajuda

  • regras mudam frequentemente;
  • muitos termos possuem significado específico;
  • várias equipes trabalham em áreas diferentes;
  • existem processos longos e exceções;
  • erros de negócio são caros;
  • o modelo é vantagem competitiva.

Quando simplificar

Um CRUD administrativo pequeno pode usar camadas simples. Aplicar agregados, factories e eventos sem complexidade real cria cerimônia.

Erros comuns

  • DDD como pastas: não há colaboração com especialistas.
  • Uma entidade por tabela: banco define o modelo.
  • Agregado gigante: contenção aumenta.
  • Modelo anêmico: regras ficam em services genéricos.
  • Eventos para tudo: complexidade cresce sem necessidade.
  • Microserviços prematuros: operação domina o domínio.
  • Linguagem inconsistente: código e negócio divergem.

Boas práticas

  • Invista no core domain.
  • Construa linguagem ubíqua.
  • Delimite contextos.
  • Proteja invariantes nos agregados.
  • Use value objects.
  • Defina repositories por raiz.
  • Separe domínio e infraestrutura.
  • Use eventos com propósito.
  • Teste exemplos do negócio.
  • Simplifique subdomínios genéricos.

Conclusão

O Domain-Driven Design no Node.js aproxima o software do conhecimento do negócio. Linguagem ubíqua, bounded contexts, agregados e value objects tornam regras explícitas e reduzem ambiguidades.

DDD traz mais valor em domínios complexos, não em todo CRUD. Quando combinado com arquitetura hexagonal, repositories, Unit of Work e eventos transacionais, ele protege o core sem prender a aplicação a frameworks ou bancos específicos.

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