DDD na Prática: Bounded Contexts sem Overengineering
Domain-Driven Design virou sinônimo de complexidade desnecessária porque a maioria das implementações ignora o conselho mais importante do próprio Evans: use DDD onde a complexidade de negócio justifica. Para um CRUD simples, é overengineering.
Quando DDD faz sentido
DDD resolve problemas de domínio complexo — regras de negócio ricas, múltiplos subdomínios com modelos diferentes, times separados trabalhando em partes distintas do sistema.
Se o domínio é complexo, o custo de NÃO usar DDD é uma base de código com modelos anêmicos, lógica de negócio espalhada em services e controllers, e regras implícitas que só existem na cabeça de quem estava lá no começo.
Identificando Bounded Contexts
Um bounded context é um limite dentro do qual um modelo específico é válido e consistente. O mesmo termo pode ter significados diferentes em contextos diferentes.
E-commerce: o conceito de "Produto"
┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐
│ Catálogo │ │ Estoque │ │ Pedidos │
│ │ │ │ │ │
│ Produto: │ │ Produto: │ │ Produto: │
│ - nome │ │ - SKU │ │ - preçoNaEpoca │
│ - descrição │ │ - quantidade │ │ - quantidade │
│ - fotos │ │ - localização │ │ - desconto aplicado │
│ - especificações │ │ - movimentações │ │ │
└─────────────────────┘ └─────────────────────┘ └─────────────────────┘O erro clássico: um único modelo Produto tentando servir todos os contextos. Ele vira um objeto com 40 campos onde metade é null dependendo do contexto.
Linguagem Ubíqua em código
// ❌ Código técnico sem linguagem de domínio
class OrderService {
async processOrder(userId: string, items: any[]) {
const user = await this.userRepo.findById(userId);
if (user.status !== 'active') throw new Error('Invalid user');
let total = 0;
for (const item of items) {
total += item.price * item.qty;
}
// ...
}
}
// ✅ Linguagem do domínio explícita no código
class Pedido {
private constructor(
readonly pedidoId: PedidoId,
readonly cliente: Cliente,
readonly itens: ItemDePedido[],
) {}
static fazer(cliente: Cliente, itens: ItemDePedido[]): Pedido {
if (!cliente.podeRealizarPedidos()) {
throw new ClienteNaoAutorizadoError(cliente.id);
}
if (itens.length === 0) {
throw new PedidoSemItensError();
}
return new Pedido(PedidoId.novo(), cliente, itens);
}
calcularTotal(): Dinheiro {
return this.itens
.map(item => item.calcularSubtotal())
.reduce((acc, val) => acc.somar(val), Dinheiro.zero());
}
confirmar(): PedidoConfirmado {
// Retorna um evento de domínio
return new PedidoConfirmado(this);
}
}Value Objects: encapsulando conceitos primitivos
// ❌ Primitive Obsession
function calcularFrete(cep: string, peso: number): number { ... }
// ✅ Value Objects com semântica rica
class CEP {
private constructor(private readonly valor: string) {}
static criar(valor: string): CEP {
const limpo = valor.replace(/\D/g, '');
if (limpo.length !== 8) throw new CEPInvalidoError(valor);
return new CEP(limpo);
}
get estado(): Estado {
const prefixo = parseInt(this.valor.substring(0, 2));
// lógica de mapeamento CEP → Estado
}
formato(): string {
return `${this.valor.substring(0,5)}-${this.valor.substring(5)}`;
}
}
class Peso {
private constructor(
private readonly valor: number,
private readonly unidade: 'kg' | 'g'
) {}
static emKg(valor: number): Peso {
if (valor <= 0) throw new PesoInvalidoError();
return new Peso(valor, 'kg');
}
get gramas(): number {
return this.unidade === 'kg' ? this.valor * 1000 : this.valor;
}
}
function calcularFrete(cep: CEP, peso: Peso): Dinheiro { ... }Aggregates: definindo limites de consistência
// Aggregate Root: Pedido
// Invariante: um pedido confirmado não pode ser modificado
class Pedido {
private _status: StatusPedido = 'rascunho';
private _itens: ItemDePedido[] = [];
private _eventos: EventoDeDominio[] = [];
adicionarItem(produto: Produto, quantidade: Quantidade): void {
if (this._status !== 'rascunho') {
throw new PedidoJaConfirmadoError();
}
const itemExistente = this._itens.find(i => i.produtoId.equals(produto.id));
if (itemExistente) {
itemExistente.aumentarQuantidade(quantidade);
} else {
this._itens.push(ItemDePedido.criar(produto, quantidade));
}
}
confirmar(pagamento: Pagamento): void {
if (this._itens.length === 0) throw new PedidoSemItensError();
if (!pagamento.foiAprovado()) throw new PagamentoNaoAprovadoError();
this._status = 'confirmado';
this._eventos.push(new PedidoConfirmado(this.id, this.calcularTotal()));
}
// Eventos acumulados para publicar após persistência
coletarEventos(): EventoDeDominio[] {
const eventos = [...this._eventos];
this._eventos = [];
return eventos;
}
}Context Map: como os bounded contexts se comunicam
Catálogo ──── ACL ────→ Pedidos
↑
Anti-Corruption Layer
(traduz entre os modelos)
class ProdutoCatalogoParaPedidosTranslator {
traduzir(produtoCatalogo: ProdutoCatalogo): ProdutoPedido {
return new ProdutoPedido(
produtoCatalogo.id,
produtoCatalogo.precoAtual, // captura preço atual
produtoCatalogo.nome
);
}
}A regra prática
Comece simples. Um serviço com módulos bem separados por domínio já aplica os princípios de DDD sem a infraestrutura pesada. Adicione Aggregates e Value Objects onde as regras de negócio são mais complexas. Reserve a arquitetura completa para os subdomínios core do sistema — onde a lógica de negócio é a vantagem competitiva.
Overengineering é aplicar DDD em um CRUD de cadastro de clientes. Underengineering é não aplicar em um motor de precificação com 50 regras de negócio.