anúncios

sexta-feira, 22 de maio de 2026

DDD explicado de forma didática

Domain-Driven Design: como modelar software complexo colocando o domínio no centro do universo

O que é DDD?

Domain-Driven Design (DDD) é uma abordagem de desenvolvimento de software criada por Eric Evans no livro azul de 2003. O nome pode assustar, mas a ideia é simples: em vez de começar pensando em bancos de dados, rotas HTTP ou frameworks, você começa pensando no domínio do negócio, ou seja, no problema que o software precisa resolver.

DDD não é uma tecnologia, nem um framework, nem uma arquitetura como MVC. É um conjunto de princípios e padrões que ajuda times de software a criar modelos mentais compartilhados com especialistas do negócio.

"O coração do software é a capacidade de resolver problemas do domínio." Eric Evans

Por que DDD existe?

Projetos de software falham com frequência porque o time técnico e os especialistas do negócio não falam a mesma língua. O desenvolvedor pergunta "qual o tipo desse campo?"; o especialista responde "é o código da agência". Ninguém se entende. O resultado são sistemas cheios de regras espalhadas, modelos anêmicos e código que ninguém consegue mudar sem quebrar tudo.

DDD propõe uma linguagem comum (chamada de Ubiquitous Language) usada por todos, do analista ao dev ao QA, e um modelo de software que reflete fielmente essa linguagem.

Os pilares do DDD

1. Linguagem Ubíqua (Ubiquitous Language)

É o vocabulário compartilhado entre todos os envolvidos. Se o especialista chama algo de "Transferência", o código deve ter uma classe Transferencia, a tabela deve se chamar transferencias, e a API deve expor /transferencias. Nada de Trans, TransfRecord ou transactions, usem o mesmo nome.

2. Domínio e Subdomínios

O domínio é o coração do negócio, é o problema principal que o sistema resolve. Um único sistema grande geralmente contém vários subdomínios:

  • Core Domain: a parte mais importante, que dá vantagem competitiva. Ex: o motor de precificação de um banco.
  • Supporting Subdomain: necessário, mas não estratégico. Ex: cálculo de impostos.
  • Generic Subdomain: pode ser comprado ou usado pronto. Ex: autenticação, envio de e-mail.
// Exemplo: o domínio de um sistema bancário
// Core Domain: Motor de crédito
class AnaliseDeCredito {
  constructor(private score: Score, 
  private rendaMonetaria: Renda) {}
  aprovar(): boolean {
      /* lógica de negócio central */
      }
}

// Supporting Subdomain: Cálculo de tarifas
class CalculadoraDeTarifas {
  calcular(operacao: Operacao): number { 
  /* complexo, mas não estratégico */ }
}

// Generic Subdomain: Autenticação (pronto, comprado)
// Usamos Keycloak, Auth0, ou similar

3. Contextos Delimitados (Bounded Contexts)

Um dos conceitos mais importantes do DDD. Um Bounded Context é uma fronteira explícita dentro da qual um modelo de domínio é válido. Dentro dela, a Linguagem Ubíqua tem significado único e consistente.

Por exemplo: no contexto de Vendas, "Cliente" tem endereço de entrega e histórico de pedidos. No contexto de Cobrança, "Cliente" tem CPF, score de crédito e data de vencimento. São modelos diferentes e devem viver em contextos separados (serviços, módulos, ou até repositórios distintos).

Cada Bounded Context pode ter sua própria arquitetura, seu próprio banco de dados e sua própria equipe. O que vale dentro de um, não vale dentro do outro.

Os blocos de construção táticos

DDD fornece padrões de modelagem para transformar a linguagem do negócio em código expressivo.

Entity

Um objeto que tem identidade própria. Dois objetos com os mesmos atributos mas identidades diferentes são entidades diferentes. Ex: um Cliente com id = 123 é diferente do cliente id = 456, mesmo que tenham o mesmo nome.

class Cliente {
  constructor(
    // identidade única
    readonly id: ClienteId,  
    private nome: Nome,
    private email: Email
  ) {}

  trocarEmail(novoEmail: Email): void {
    this.email = novoEmail;
  }
}

Value Object

Um objeto que não tem identidade, é definido apenas pelos seus atributos. Dois Value Objects com os mesmos valores são considerados iguais. São imutáveis.

  • Dinheiro (valor + moeda), R$ 50,00 não deixa de ser R$ 50,00 por ter "identidade".
  • CPF, Email, Endereco, todos imutáveis e comparados por valor.
class Dinheiro {
  constructor(
    readonly valor: number,
     // 'BRL', 'USD'
    readonly moeda: string 
  ) {}

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

  equals(outro: Dinheiro): boolean {
    return this.valor === outro.valor && 
               this.moeda === outro.moeda;
  }
}

Aggregate

Um cluster de objetos tratados como uma unidade. Cada Aggregate tem uma raiz (Aggregate Root) que é a única porta de entrada para o mundo externo. Toda consistência do cluster passa pela raiz.

// Aggregate Root: Pedido
class Pedido {
  constructor(
    readonly id: PedidoId,
    readonly clienteId: ClienteId,
    private itens: Item[],
    private status: StatusPedido
  ) {}

  adicionarItem(produto: Produto, 
    quantidade: number): void {
    if (this.status !== 'aberto')
      throw new Error('Pedido fechado não aceita itens');
    this.itens.push(new Item(produto, quantidade));
  }

  total(): Dinheiro {
    return this.itens.reduce(
      (acc, item) => acc.somar(item.subtotal()),
      new Dinheiro(0, 'BRL')
    );
  }
}

Domain Event

Algo que aconteceu no domínio e interessa a outras partes do sistema. Usa-se verbos no passado: PedidoCriado, TransferenciaRealizada, ContaEncerrada.

class PedidoCriado {
  constructor(
    readonly pedidoId: PedidoId,
    readonly clienteId: ClienteId,
    readonly total: Dinheiro,
    readonly ocorridoEm: Date = new Date()
  ) {}
}

Repository

Abstração de persistência. Para o domínio, o Repository parece um coleção em memória. O domínio nunca sabe se está usando PostgreSQL, MongoDB ou arquivo JSON.

interface PedidoRepository {
  salvar(pedido: Pedido): Promise<void>;
  buscarPorId(id: PedidoId): Promise<Pedido | null>;
  buscarPorCliente(clienteId: ClienteId): Promise<
                                          Pedido[]>;
}

Domain Service

Quando uma operação não pertence naturalmente a uma Entity ou Value Object, criamos um Domain Service. Ele orquestra regras que envolvem múltiplos agregados.

class ServicoDeTransferencia {
  constructor(
    private contas: ContaRepository
  ) {}

  transferir(origemId: ContaId, destinoId: 
  ContaId, valor: Dinheiro): void {
    const origem = this.contas.buscarPorId(origemId);
    const destino = this.contas.buscarPorId(destinoId);

    origem.debitar(valor);
    destino.creditar(valor);

    this.contas.salvar(origem);
    this.contas.salvar(destino);
  }
}

DDD e arquitetura

DDD não exige uma arquitetura específica, mas se encaixa perfeitamente com Arquitetura Hexagonal (Ports & Adapters) e Clean Architecture. A regra de ouro é:

  • O domínio fica no centro, isolado de frameworks, banco de dados e UI.
  • A infraestrutura (HTTP, banco, filas) fica nas bordas e depende do domínio, não o contrário.
  • As dependências apontam para dentro (Dependency Inversion Principle).
src/
├── dominio/    # CORE  sem dependências externas
│   ├── entidades/
│   ├── value-objects/
│   ├── servicos/
│   └── repositorios/ # Interfaces apenas
├── aplicacao/        # Casos de uso (orquestração)
├── infra/            # Implementações concretas
│   ├── persistencia/
│   ├── http/
│   └── fila/
└── shared/           # Código compartilhado

Quando usar DDD?

  • Use quando o domínio do negócio é complexo e cheio de regras.
  • Use quando há especialistas de negócio dispostos a conversar com o time técnico.
  • Não use em CRUDs simples um formulário que só insere dados no banco não precisa de DDD.
  • Não use se o time não tem disciplina para manter a Linguagem Ubíqua viva.
  • Não use como dogma DDD é ferramenta, não religião.

Passos práticos para começar

  1. Entreviste especialistas sente com quem entende do negócio e faça perguntas. Anote os termos que eles usam.
  2. Crie um glossário monte a Linguagem Ubíqua com definições claras de cada termo.
  3. Identifique os Bounded Contexts desenhe fronteiras: o que faz parte de Vendas? O que faz parte de Cobrança?
  4. Modele os Aggregates descubra as raízes e o que pertence a cada uma.
  5. Implemente com TDD o domínio é a parte mais testável do sistema. Teste as regras de negócio sem tocar em banco ou HTTP.
  6. Refatore sem medo o modelo de domínio evolve com o negócio. DDD favorece a mudança.

Exemplo completo: Aluguel de Carros

Vamos modelar um pequeno domínio de locação de veículos usando DDD:

// ——— Value Objects ———
class Placa {
  constructor(readonly valor: string) {
    if (!/^[A-Z]{3}\d[A-Z]\d{2}$/.test(valor))
      throw new Error('Placa inválida');
  }
}

class Quilometragem {
  constructor(readonly valor: number) {
    if (valor < 0) throw new 
    Error('KM não pode ser negativo');
  }
}

class Periodo {
  constructor(readonly inicio: Date, 
  readonly fim: Date) {
    if (fim <= inicio) throw new 
    Error('Período inválido');
  }
  duracaoDias(): number {
    return (this.fim.getTime() - 
    this.inicio.getTime()) / 86400000;
  }
}

// ——— Aggregate: Veiculo ———
class Veiculo {
  constructor(
    readonly id: VeiculoId,
    readonly placa: Placa,
    private kmAtual: Quilometragem,
    private disponivel: boolean = true
  ) {}

  alugar(): void {
    if (!this.disponivel) throw new 
    Error('Veículo indisponível');
    this.disponivel = false;
  }

  registrarDevolucao(kmFinal: Quilometragem): void {
    this.disponivel = true;
    this.kmAtual = kmFinal;
  }
}

// ——— Aggregate: Locacao ———
class Locacao {
  constructor(
    readonly id: LocacaoId,
    readonly veiculoId: VeiculoId,
    readonly clienteId: ClienteId,
    readonly periodo: Periodo,
    readonly tarifaDiaria: Dinheiro,
    private status: 'ativa' | 'finalizada' = 'ativa'
  ) {}

  calcularTotal(): Dinheiro {
    const dias = this.periodo.duracaoDias();
    return new Dinheiro(dias * 
           this.tarifaDiaria.valor, 'BRL');
  }

  finalizar(): void {
    if (this.status === 'finalizada')
      throw new Error('Locação já finalizada');
    this.status = 'finalizada';
  }
}

// ——— Domain Event ———
class VeiculoAlugado {
  constructor(
    readonly veiculoId: VeiculoId,
    readonly locacaoId: LocacaoId,
    readonly clienteId: ClienteId
  ) {}
}

// ——— Repository (interface) ———
interface VeiculoRepository {
  salvar(veiculo: Veiculo): Promise<void>;
  buscarDisponivel(): Promise<Veiculo[]>;
  buscarPorId(id: VeiculoId): Promise<Veiculo | null>;
}

Para se aprofundar

  • Domain-Driven Design: Tackling Complexity in the Heart of Software Eric Evans (o "livro azul")
  • Implementing Domain-Driven Design Vaughn Vernon (o "livro vermelho")
  • Domain-Driven Design Distilled Vaughn Vernon (resumo prático, ótimo para começar)
  • DDD: The First 15 Years Artigos de diversos autores sobre a evolução do DDD

Considerações finais

DDD não é sobre diagramas bonitos ou arquitetura sofisticada. É sobre comunicação. É sobre criar um modelo de software que qualquer pessoa do negócio consegue ler e validar. É sobre colocar a complexidade do domínio no centro e usar padrões que a tornem controlável.

Comece pequeno: escolha um Bounded Context, crie a Linguagem Ubíqua com um especialista, modele alguns Aggregates e escreva testes. O resto vem com a prática.

DDD é uma jornada, não um destino.

Feito!

Nenhum comentário:

Postar um comentário