DDD: Bounded Contexts - Delimitando contextos (Parte 3)
Este é o terceiro artigo da série sobre Domain-Driven Design. Nos artigos anteriores, exploramos os conceitos fundamentais do DDD e a importância da Linguagem Ubíqua. Agora vamos mergulhar em um dos conceitos mais poderosos e práticos do DDD: os Bounded Contexts (Contextos Delimitados).
Se você já tentou aplicar um modelo único para todo um sistema complexo, provavelmente descobriu que isso rapidamente se torna inviável. Os Bounded Contexts são a resposta do DDD para esse problema, permitindo que dividamos sistemas grandes em partes menores e mais manejáveis.
O que são Bounded Contexts?
Bounded Context é um padrão que define uma fronteira organizacional e linguística explícita dentro da qual um modelo de domínio específico é definido e aplicável. Em outras palavras, é o limite dentro do qual uma determinada linguagem ubíqua faz sentido e onde um conjunto específico de conceitos, terminologia e regras de negócio se aplicam.
Como Eric Evans define:
"Bounded Context delimita a aplicabilidade de um modelo particular. Dentro do contexto, todos os termos e frases da Linguagem Ubíqua têm significado específico, e o modelo reflete a linguagem com precisão."
O Problema: A Busca pelo Modelo Único
Cenário Real: E-commerce
Imagine um sistema de e-commerce onde tentamos usar um único modelo para "Cliente". Diferentes partes do sistema têm necessidades diferentes:
No contexto de Vendas:
- Cliente precisa de: nome, CPF, histórico de compras, dados de cobrança
- Regras: desconto por fidelidade, limite de crédito
No contexto de Marketing:
- Cliente precisa de: preferências, comportamento de navegação, segmentação
- Regras: campanhas direcionadas, análise de persona
No contexto de Suporte:
- Cliente precisa de: tickets abertos, histórico de problemas, satisfação
- Regras: escalação de tickets, SLA de atendimento
No contexto de Logística:
- Cliente precisa de: endereços de entrega, preferências de entrega
- Regras: cálculo de frete, zones de entrega
O Resultado: O Modelo Frankenstein
Tentando atender todos os contextos com um modelo único, acabamos com algo assim:
<?php
// ❌ O "modelo que faz tudo"
class Cliente
{
// Dados pessoais (Vendas)
private string $nome;
private string $cpf;
private string $email;
private string $telefone;
// Dados financeiros (Vendas)
private float $limiteCredito;
private array $historicoCompras;
// Dados de marketing (Marketing)
private array $preferencias;
private ComportamentoNavegacao $comportamentoNavegacao;
private array $segmentos;
// Dados de suporte (Suporte)
private array $tickets;
private int $nivelSatisfacao;
private array $historicoProblemas;
// Dados logísticos (Logística)
private array $enderecosEntrega;
private PreferenciaEntrega $preferenciasEntrega;
private ZonaEntrega $zonaEntrega;
// Métodos misturados
public function calcularDesconto(): float { /* vendas */ }
public function enviarCampanha(): void { /* marketing */ }
public function escalarTicket(): void { /* suporte */ }
public function calcularFrete(): float { /* logística */ }
}
Este modelo está destinado ao fracasso porque:
- Acoplamento alto: Mudança em um contexto afeta todos os outros
- Responsabilidades misturadas: Uma classe faz tudo
- Complexidade crescente: Cada novo requisito torna o modelo mais complexo
- Equipes bloqueadas: Mudanças requerem coordenação entre todas as equipes
A Solução: Bounded Contexts
Dividindo em Contextos
Aplicando Bounded Contexts, dividimos o sistema em contextos menores e mais coesos:
<?php
// ✅ Contexto de Vendas
namespace Vendas;
class Cliente
{
private string $id;
private string $nome;
private string $cpf;
private Dinheiro $limiteCredito;
private array $historicoCompras;
public function calcularDesconto(): Dinheiro
{
// Lógica específica de vendas
return count($this->historicoCompras) > 10
? new Dinheiro(100, 'BRL')
: Dinheiro::zero('BRL');
}
public function podeComprar(Dinheiro $valor): bool
{
return $valor->menorOuIgualA($this->limiteCredito);
}
}
// ✅ Contexto de Marketing
namespace Marketing;
class Cliente
{
private string $id;
private string $email;
private array $preferencias;
private array $segmentos;
public function adicionarSegmento(Segmento $segmento): void
{
if (!$this->pertenceSegmento($segmento)) {
$this->segmentos[] = $segmento;
}
}
public function elegparaParaCampanha(Campanha $campanha): bool
{
foreach ($this->segmentos as $segmento) {
if ($campanha->direcionadaPara($segmento)) {
return true;
}
}
return false;
}
}
// ✅ Contexto de Suporte
namespace Suporte;
class Cliente
{
private string $id;
private string $nome;
private string $email;
private array $tickets;
private NivelSatisfacao $nivelSatisfacao;
public function abrirTicket(string $descricao, Prioridade $prioridade): Ticket
{
$ticket = new Ticket($this->id, $descricao, $prioridade);
$this->tickets[] = $ticket;
return $ticket;
}
public function isClienteVip(): bool
{
return $this->nivelSatisfacao->isAlto() && count($this->tickets) < 3;
}
}
Características dos Bounded Contexts
1. Autonomia
Cada contexto é autônomo e pode evoluir independentemente:
<?php
// Contexto de Vendas pode mudar sua lógica sem afetar Marketing
namespace Vendas;
class Cliente
{
// Vendas decide usar pontos de fidelidade em vez de desconto direto
public function calcularDesconto(): Dinheiro
{
return $this->pontosFidelidade->converterParaDesconto();
}
}
// Marketing continua funcionando normalmente
namespace Marketing;
class Cliente
{
// Não é afetado pela mudança em Vendas
public function elegparaParaCampanha(Campanha $campanha): bool
{
foreach ($this->segmentos as $segmento) {
if ($campanha->direcionadaPara($segmento)) {
return true;
}
}
return false;
}
}
2. Linguagem Específica
Cada contexto tem sua própria linguagem ubíqua:
<?php
// No contexto de Biblioteca
namespace Biblioteca;
class Usuario
{
private array $emprestimosAtivos;
public function podeEmprestar(): bool
{
return count($this->emprestimosAtivos) < 3;
}
}
// No contexto de Cafeteria (na mesma biblioteca)
namespace Cafeteria;
class Cliente
{
private Dinheiro $contaCredito;
public function podeComprar(Dinheiro $valor): bool
{
return $this->contaCredito->maiorOuIgualA($valor);
}
}
3. Modelos Diferentes para o Mesmo Conceito
O mesmo conceito real pode ter modelos completamente diferentes:
// Sistema Hospitalar
// Contexto de Consultas
namespace Consultas {
class Paciente {
private sintomas: Sintoma[];
private historicoMedico: HistoricoMedico;
agendarConsulta(medico: Medico, data: Date): Consulta {
return new Consulta(this, medico, data);
}
}
}
// Contexto de Faturamento
namespace Faturamento {
class Paciente {
private dadosCobranca: DadosCobranca;
private convenio: Convenio;
calcularValorConsulta(procedimento: Procedimento): Money {
return this.convenio.calcularCobertura(procedimento);
}
}
}
// Contexto de Farmácia
namespace Farmacia {
class Paciente {
private alergias: Alergia[];
private medicamentosAtuais: Medicamento[];
podeTomarMedicamento(medicamento: Medicamento): boolean {
return !this.temAlergiaA(medicamento) &&
!this.temInteracao(medicamento);
}
}
}
Identificando Bounded Contexts
Sinais de que Você Precisa de Novos Contextos
1. Dados "Inúteis" nos Testes
// ❌ Sinal de problema
describe('Calculo de Frete', () => {
it('deve calcular frete corretamente', () => {
const cliente = new Cliente(
'João',
'123.456.789-00',
'joao@email.com',
1000, // limite crédito - não usado neste teste
[], // histórico compras - não usado
[], // preferências marketing - não usado
5, // satisfação - não usado
new Endereco('SP', 'São Paulo', '01234-567')
);
const frete = calculadoraFrete.calcular(cliente);
expect(frete.valor).toBe(15.00);
});
});
2. Modelos Inchados
<?php
// ❌ Modelo que faz muita coisa
class Produto
{
// Dados do catálogo
private string $nome;
private string $descricao;
private Categoria $categoria;
// Dados de estoque
private int $quantidadeEstoque;
private int $estoqueMinimo;
// Dados de preço
private Dinheiro $preco;
private array $promocoes;
// Dados de avaliação
private array $avaliacoes;
private float $notaMedia;
// Dados de recomendação
private string $algoritmoRecomendacao;
private array $produtosRelacionados;
// Métodos misturados - muitas responsabilidades!
public function atualizarEstoque(): void { }
public function calcularPrecoComDesconto(): Dinheiro { }
public function adicionarAvaliacao(): void { }
public function recomendarProdutos(): array { }
}
3. Mudanças que Afetam Múltiplas Áreas
Se uma mudança simples requer alteração em várias partes não relacionadas do sistema, provavelmente os contextos não estão bem definidos.
Técnicas para Descobrir Contextos
1. Event Storming
Mapear eventos de domínio ajuda a identificar contextos naturais:
Timeline de E-commerce:
[Cliente Registrou] → [Produto Visualizado] → [Item Adicionado ao Carrinho]
→ [Checkout Iniciado] → [Pagamento Processado] → [Pedido Confirmado]
→ [Item Separado] → [Produto Enviado] → [Entrega Realizada]
Contextos identificados:
- Catálogo: Produto Visualizado
- Carrinho: Item Adicionado, Checkout Iniciado
- Pagamento: Pagamento Processado
- Pedidos: Pedido Confirmado
- Estoque: Item Separado
- Logística: Produto Enviado, Entrega Realizada
2. Análise de Linguagem
Preste atenção quando a mesma palavra significa coisas diferentes:
"Pedido" em diferentes contextos:
Vendas: "Pedido é uma solicitação de compra do cliente"
- Pedido tem itens, preço, desconto
Estoque: "Pedido é uma lista de itens para separação"
- Pedido tem localização no estoque, quantidade
Logística: "Pedido é um pacote para entrega"
- Pedido tem endereço, peso, dimensões
Financeiro: "Pedido é uma transação financeira"
- Pedido tem forma de pagamento, valor, imposto
3. Análise Organizacional
Contexts frequentemente seguem limites organizacionais:
Organização de E-commerce:
Equipe de Produto → Contexto de Catálogo
Equipe de Vendas → Contexto de Pedidos
Equipe de Marketing → Contexto de CRM
Equipe de Logística → Contexto de Entrega
Equipe Financeira → Contexto de Pagamentos
Bounded Contexts e Microservices
Bounded Contexts fornecem fronteiras naturais para microservices:
Mapeamento Direto
// Microservice de Catálogo
@Service
class CatalogoService {
// Implementa o Bounded Context de Catálogo
async buscarProdutos(filtros: FiltrosProduto): Promise<Produto[]> {
// Lógica específica do contexto de catálogo
}
async adicionarProduto(produto: Produto): Promise<void> {
// Validações específicas do contexto
}
}
// Microservice de Pedidos
@Service
class PedidosService {
// Implementa o Bounded Context de Pedidos
async criarPedido(itens: ItemPedido[]): Promise<Pedido> {
// Lógica específica do contexto de pedidos
}
async confirmarPedido(pedidoId: string): Promise<void> {
// Regras específicas do contexto
}
}
Comunicação Entre Contextos
// Integração via eventos
class PedidoConfirmadoEvent {
constructor(
public pedidoId: string,
public clienteId: string,
public itens: ItemPedido[],
public valor: Money
) {}
}
// Contexto de Pedidos publica evento
class PedidosService {
async confirmarPedido(pedidoId: string): Promise<void> {
const pedido = await this.repository.buscar(pedidoId);
pedido.confirmar();
// Publica evento para outros contextos
await this.eventBus.publish(
new PedidoConfirmadoEvent(
pedido.id,
pedido.clienteId,
pedido.itens,
pedido.valor
)
);
}
}
// Contexto de Estoque reage ao evento
class EstoqueService {
@EventHandler(PedidoConfirmadoEvent)
async quando_pedido_confirmado(evento: PedidoConfirmadoEvent): Promise<void> {
for (const item of evento.itens) {
await this.reservarItem(item.produtoId, item.quantidade);
}
}
}
Padrões de Relacionamento Entre Contextos
1. Partnership (Parceria)
Dois contextos evoluem juntos com coordenação mútua:
// Contextos de Carrinho e Preço evoluem juntos
interface CarrinhoPrecoIntegration {
// Interface compartilhada e evoluída em conjunto
calcularTotalCarrinho(itens: ItemCarrinho[]): Money;
}
2. Shared Kernel (Núcleo Compartilhado)
Compartilham um conjunto de código comum:
// Núcleo compartilhado entre contextos
namespace NucleoCompartilhado {
export class Money {
constructor(
public valor: number,
public moeda: string
) {}
somar(outro: Money): Money {
if (this.moeda !== outro.moeda) {
throw new Error('Moedas diferentes');
}
return new Money(this.valor + outro.valor, this.moeda);
}
}
export class Email {
constructor(private endereco: string) {
if (!this.isValido(endereco)) {
throw new Error('Email inválido');
}
}
private isValido(email: string): boolean {
return /\S+@\S+\.\S+/.test(email);
}
}
}
3. Customer-Supplier (Cliente-Fornecedor)
Um contexto fornece serviços para outro:
// Contexto de Identidade (Supplier)
class IdentidadeService {
async validarToken(token: string): Promise<Usuario | null> {
// Implementação do fornecedor
}
}
// Contexto de Pedidos (Customer)
class PedidosService {
constructor(private identidade: IdentidadeService) {}
async criarPedido(token: string, itens: ItemPedido[]): Promise<Pedido> {
const usuario = await this.identidade.validarToken(token);
if (!usuario) {
throw new Error('Token inválido');
}
// Continua com a criação do pedido
}
}
4. Anti-Corruption Layer (Camada Anti-Corrupção)
Protege um contexto de mudanças em sistemas externos:
// Sistema externo de pagamento
interface SistemaPagamentoExterno {
processPayment(cardNum: string, amt: number): PaymentResult;
}
// Anti-Corruption Layer
class AdaptadorPagamento {
constructor(private sistemaExterno: SistemaPagamentoExterno) {}
async processarPagamento(pagamento: PagamentoDominio): Promise<ResultadoPagamento> {
// Traduz do modelo interno para o externo
const resultado = await this.sistemaExterno.processPayment(
pagamento.cartao.numero,
pagamento.valor.quantidade
);
// Traduz do modelo externo para o interno
return new ResultadoPagamento(
resultado.success,
resultado.transactionId,
new Money(resultado.amount, 'BRL')
);
}
}
// Contexto de Pagamento usa o adaptador
class PagamentoService {
constructor(private adaptador: AdaptadorPagamento) {}
async processarPagamento(pagamento: PagamentoDominio): Promise<ResultadoPagamento> {
return await this.adaptador.processarPagamento(pagamento);
}
}
Exemplo Prático: Sistema de Entrega
Vamos aplicar Bounded Contexts em um sistema de delivery:
Contextos Identificados
// 1. Contexto de Cardápio
namespace Cardapio {
class Produto {
constructor(
private id: string,
private nome: string,
private descricao: string,
private preco: Money,
private categoria: Categoria,
private disponivel: boolean
) {}
marcarIndisponivel(): void {
this.disponivel = false;
}
}
class Categoria {
constructor(
private nome: string,
private ordem: number
) {}
}
}
// 2. Contexto de Pedidos
namespace Pedidos {
class Pedido {
private itens: ItemPedido[] = [];
private status: StatusPedido = StatusPedido.PENDENTE;
constructor(
private id: string,
private clienteId: string,
private restauranteId: string
) {}
adicionarItem(produtoId: string, quantidade: number, preco: Money): void {
const item = new ItemPedido(produtoId, quantidade, preco);
this.itens.push(item);
}
confirmar(): void {
if (this.itens.length === 0) {
throw new Error('Pedido vazio não pode ser confirmado');
}
this.status = StatusPedido.CONFIRMADO;
}
calcularTotal(): Money {
return this.itens.reduce(
(total, item) => total.somar(item.calcularSubtotal()),
Money.zero('BRL')
);
}
}
class ItemPedido {
constructor(
private produtoId: string,
private quantidade: number,
private precoUnitario: Money
) {}
calcularSubtotal(): Money {
return this.precoUnitario.multiplicar(this.quantidade);
}
}
}
// 3. Contexto de Entrega
namespace Entrega {
class Entrega {
private status: StatusEntrega = StatusEntrega.AGUARDANDO_ENTREGADOR;
constructor(
private id: string,
private pedidoId: string,
private enderecoDestino: Endereco,
private tempoEstimado: number
) {}
atribuirEntregador(entregadorId: string): void {
this.entregadorId = entregadorId;
this.status = StatusEntrega.A_CAMINHO;
}
marcarComoEntregue(): void {
this.status = StatusEntrega.ENTREGUE;
this.dataEntrega = new Date();
}
calcularTempoReal(): number {
if (!this.dataEntrega) return 0;
return this.dataEntrega.getTime() - this.dataInicio.getTime();
}
}
class Entregador {
constructor(
private id: string,
private nome: string,
private posicaoAtual: Coordenada,
private disponivel: boolean
) {}
aceitar entrega(entrega: Entrega): void {
if (!this.disponivel) {
throw new Error('Entregador não está disponível');
}
this.disponivel = false;
entrega.atribuirEntregador(this.id);
}
}
}
// 4. Contexto de Pagamento
namespace Pagamento {
class Pagamento {
private status: StatusPagamento = StatusPagamento.PENDENTE;
constructor(
private id: string,
private pedidoId: string,
private valor: Money,
private metodoPagamento: MetodoPagamento
) {}
processar(): void {
// Lógica específica de pagamento
this.status = StatusPagamento.PROCESSADO;
}
estornar(): void {
if (this.status !== StatusPagamento.PROCESSADO) {
throw new Error('Só é possível estornar pagamentos processados');
}
this.status = StatusPagamento.ESTORNADO;
}
}
}
Coordenação Entre Contextos
// Orquestração via eventos
class PedidoWorkflow {
constructor(
private pedidosService: PedidosService,
private pagamentoService: PagamentoService,
private entregaService: EntregaService,
private eventBus: EventBus
) {}
@EventHandler(PedidoConfirmadoEvent)
async quando_pedido_confirmado(evento: PedidoConfirmadoEvent): Promise<void> {
// Inicia pagamento
await this.pagamentoService.processarPagamento(
evento.pedidoId,
evento.valor,
evento.metodoPagamento
);
}
@EventHandler(PagamentoProcessadoEvent)
async quando_pagamento_processado(evento: PagamentoProcessadoEvent): Promise<void> {
// Inicia entrega
await this.entregaService.criarEntrega(
evento.pedidoId,
evento.enderecoEntrega
);
}
@EventHandler(EntregaFinalizadaEvent)
async quando_entrega_finalizada(evento: EntregaFinalizadaEvent): Promise<void> {
// Finaliza pedido
await this.pedidosService.finalizarPedido(evento.pedidoId);
}
}
Armadilhas Comuns
1. Contextos Muito Pequenos
// ❌ Contexto pequeno demais
namespace ValidacaoEmail {
class ValidadorEmail {
validar(email: string): boolean {
return /\S+@\S+\.\S+/.test(email);
}
}
}
2. Contextos Muito Grandes
// ❌ Contexto grande demais - faz muita coisa
namespace ComercioEletronico {
class Sistema {
// Mistura catálogo, pedidos, pagamento, entrega...
buscarProdutos(): Produto[] { }
criarPedido(): Pedido { }
processarPagamento(): void { }
calcularFrete(): Money { }
rastrearEntrega(): string { }
}
}
3. Dependências Circulares
// ❌ Dependência circular entre contextos
namespace Pedidos {
class PedidoService {
constructor(private estoque: EstoqueService) {} // ❌
}
}
namespace Estoque {
class EstoqueService {
constructor(private pedidos: PedidoService) {} // ❌
}
}
Melhores Práticas
1. Comece com Contextos Maiores
// ✅ Comece com contexto maior e refine depois
namespace ECommerce {
// Implemente tudo aqui primeiro
// Depois extraia contextos menores conforme necessário
}
2. Use Eventos para Comunicação
// ✅ Comunicação assíncrona via eventos
class EstoqueService {
@EventHandler(PedidoConfirmadoEvent)
async quando_pedido_confirmado(evento: PedidoConfirmadoEvent): Promise<void> {
// Reage ao evento sem acoplar com contexto de pedidos
}
}
3. Mantenha dados Consistentes Dentro do Contexto
// ✅ Consistência dentro do bounded context
namespace Pedidos {
class PedidoService {
@Transactional
async confirmarPedido(pedidoId: string): Promise<void> {
const pedido = await this.repository.buscar(pedidoId);
pedido.confirmar();
// Tudo dentro da mesma transação
await this.repository.salvar(pedido);
await this.eventBus.publish(new PedidoConfirmadoEvent(pedido));
}
}
}
4. Use Anti-Corruption Layers para Sistemas Externos
// ✅ ACL protege o contexto de mudanças externas
class AdaptadorSistemaLegado {
async buscarCliente(id: string): Promise<Cliente> {
const dadosLegado = await this.sistemaLegado.getCustomer(id);
// Traduz modelo legado para modelo do contexto
return new Cliente(
dadosLegado.custId,
dadosLegado.custName,
new Email(dadosLegado.emailAddr)
);
}
}
Bounded Contexts na Prática
Sinais de Contextos Bem Definidos
- Equipes podem trabalhar independentemente
- Mudanças ficam isoladas dentro do contexto
- A linguagem ubíqua é clara e específica
- Os modelos são coesos e têm responsabilidades claras
- A comunicação entre contextos é explícita
Evolução dos Contextos
// Contexto pode evoluir e ser refinado
namespace PedidosV1 {
// Versão inicial mais simples
class Pedido {
itens: Item[];
total: number;
}
}
// Depois evolui para algo mais sofisticado
namespace PedidosV2 {
class Pedido {
private itens: ItemPedido[];
private status: StatusPedido;
private politicaCancelamento: PoliticaCancelamento;
calcularTotal(): Money {
return this.itens.reduce((total, item) =>
total.somar(item.calcularSubtotal()),
Money.zero('BRL')
);
}
}
}
Conclusão
Bounded Contexts são uma das ferramentas mais poderosas do DDD para lidar com a complexidade de sistemas grandes. Eles permitem:
- Dividir sistemas complexos em partes menores e manejáveis
- Reduzir acoplamento entre diferentes áreas do sistema
- Aumentar coesão dentro de cada contexto
- Facilitar evolução independente de diferentes partes
- Clarificar responsabilidades e ownership das equipes
- Definir fronteiras naturais para microservices
O segredo está em encontrar o equilíbrio certo: contextos nem muito pequenos (que criam complexidade desnecessária) nem muito grandes (que anulam os benefícios da separação).
No próximo artigo, vamos explorar como implementar esses conceitos através dos Value Objects e Entities, os building blocks fundamentais do design tático do DDD.
Referências
- Domain-Driven Design: Tackling Complexity in the Heart of Software - Eric Evans
- Building Domain Driven Microservices - Walmart Global Tech
- Using bounded context for effective domain-driven design - TechTarget
- Understanding the Bounded Context in Microservices - Bits and Pieces
- The Power of Bounded Contexts in Software Development - LinkedIn
- Adaptive, Socio-Technical Systems with Architecture for Flow - InfoQ
- DDD Part 1: Strategic Domain-Driven Design - Vaadin