Saltar para o conteúdo
DDD na Prática: Bounded Contexts sem Overengineering
Arquitetura de Software

DDD na Prática: Bounded Contexts sem Overengineering

18 de setembro de 2024·Paulo de Paula

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.