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
- Entreviste especialistas sente com quem entende do negócio e faça perguntas. Anote os termos que eles usam.
- Crie um glossário monte a Linguagem Ubíqua com definições claras de cada termo.
- Identifique os Bounded Contexts desenhe fronteiras: o que faz parte de Vendas? O que faz parte de Cobrança?
- Modele os Aggregates descubra as raízes e o que pertence a cada uma.
- 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.
- 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