Se os padrões táticos de DDD respondem à pergunta "como implemento as regras de negócio dentro de um contexto?", os padrões estratégicos respondem à pergunta anterior: "como divido o sistema em contextos e como esses contextos se relacionam?". O DDD estratégico é menos sobre código e mais sobre comunicação, fronteiras organizacionais, e a natureza da linguagem que diferentes partes de um sistema usam para descrever o mundo.
Evans dedicou a segunda metade de seu livro a essa dimensão estratégica — e muitos times que adotaram DDD nunca chegaram lá, ficando apenas com os padrões táticos. O resultado é implementações técnicas corretas dentro de contextos que foram delimitados de forma errada, resultando em acoplamento implícito, ambiguidade de vocabulário, e integrações difíceis de manter.
Ubiquitous Language — mesma palavra, mesmo significado
O conceito mais fundamental do DDD estratégico é também o mais simples de enunciar e o mais difícil de manter: Ubiquitous Language (Linguagem Ubíqua) é a linguagem compartilhada entre desenvolvedores e especialistas de domínio dentro de um contexto. Não é jargão técnico, não é linguagem de negócio genérica — é o vocabulário específico e preciso que descreve como o negócio funciona naquele contexto.
A palavra "conta" em um sistema bancário pode significar coisas muito diferentes em contextos diferentes: conta corrente no contexto de banking, conta de usuário no contexto de autenticação, conta a pagar no contexto de financeiro. Quando desenvolvedores e especialistas de negócio usam a mesma palavra para conceitos diferentes sem perceberem, surgem bugs e mal-entendidos que são difíceis de diagnosticar porque o problema não está no código — está na comunicação.
A Ubiquitous Language resolve isso exigindo que o vocabulário seja explícito e compartilhado dentro de cada contexto. O mesmo conceito tem o mesmo nome para todos. E quando a linguagem muda (o negócio usa "pedido" onde antes usava "solicitação"), o código muda também — porque código que usa a linguagem errada é código que vai mentir sobre o que faz.
Bounded Context — a fronteira linguística
Um Bounded Context é o limite dentro do qual uma Ubiquitous Language é válida. É onde um modelo de domínio específico tem significado consistente. Fora desse limite, as palavras podem significar coisas diferentes.
A ideia central é que sistemas grandes e complexos não podem ter um único modelo de domínio que seja correto para todas as partes do sistema. O modelo que faz sentido para o time de Vendas é diferente do modelo que faz sentido para o time de Logística. "Produto" em Vendas é uma entidade com preço e promoções. "Produto" em Logística é uma entidade com peso, dimensões, e restrições de armazenamento. Tentar criar um modelo único que satisfaça os dois resulta em um modelo com muitos atributos opcionais, muita condicionalidade, e que não pertence a nenhum dos dois domínios de verdade.
Bounded Contexts não são apenas conceituais — eles têm implementação física. Cada Bounded Context tem seu próprio código-base (ou módulo separado), seu próprio banco de dados (ou schema separado), sua própria pipeline de deploy idealmente. Quando dois Bounded Contexts precisam se comunicar, eles fazem isso através de interfaces bem definidas — não através de acesso direto ao banco um do outro.
Um Bounded Context é um conceito de domínio; um microservice é uma decisão de deployment. Eles frequentemente coincidem, mas não necessariamente. Um Bounded Context pode ser implementado como um módulo de monolito modular. Um microservice pode conter mais de um Bounded Context (embora seja incomum e geralmente problemático).
Context Map — o mapa das relações
Um Context Map documenta como os Bounded Contexts de um sistema se relacionam entre si. Evans definiu um conjunto de padrões de integração que descrevem a natureza dessas relações. Conhecê-los é essencial para tomar decisões conscientes sobre como integrar contextos — em vez de deixar as integrações emergirem de forma ad hoc.
Shared Kernel. Dois contextos compartilham uma parte do modelo — um subset do domínio que ambos usam. Qualquer mudança no Shared Kernel requer coordenação entre os dois times. Esse padrão reduz duplicação mas aumenta acoplamento. Indicado quando dois contextos muito próximos precisam de consistência estrita em conceitos compartilhados.
Customer/Supplier. Um contexto downstream (cliente) depende de um contexto upstream (fornecedor). O upstream define a interface; o downstream se adapta. A relação tem uma estrutura de poder: o upstream pode mudar a interface sem necessariamente considerar o downstream. Comum quando um time centralizado provê serviços para múltiplos times.
Conformist. O downstream segue o modelo do upstream sem questionar — não há poder de negociação. O time downstream se conforma ao modelo do upstream mesmo que ele não seja ideal para o downstream. Acontece quando o upstream é um sistema externo (um SaaS, uma API de terceiro) que não pode ser modificado.
Anti-Corruption Layer (ACL). O downstream cria uma camada de tradução entre o modelo do upstream e seu próprio modelo. Mesmo recebendo dados no formato do upstream, o downstream trabalha internamente com seu próprio vocabulário. Essencial quando o modelo do upstream é ruim (legado, externo, incompatível) e você não quer poluir seu modelo com ele.
Open Host Service. O upstream provê uma API bem documentada e estável que qualquer downstream pode usar. O upstream assume responsabilidade por manter a compatibilidade. Indicado quando um contexto é acessado por muitos outros — o time de Autenticação que provê tokens para todos os outros times.
Published Language. Como Open Host Service, mas com um formato de dados padronizado (JSON Schema, Protobuf, AsyncAPI) que os consumidores podem usar para gerar clientes ou validadores. A linguagem publicada é a API pública do contexto.
Separate Ways. Dois contextos não precisam se integrar — cada um resolve seu problema independentemente, mesmo que isso signifique duplicação. Indicado quando a integração custaria mais do que a duplicação.
Como identificar Bounded Contexts
A técnica mais usada para descobrir Bounded Contexts é o Event Storming (Brandolini, 2013) — uma sessão de modelagem colaborativa com desenvolvedores e especialistas de domínio onde Domain Events são identificados em um fluxo temporal. Clusters de eventos que usam o mesmo vocabulário e têm as mesmas regras de negócio tendem a apontar para o mesmo Bounded Context.
Um sinal prático de que um único contexto foi definido de forma grande demais: muitas condicionais no código que verificam "mas se for um pedido de B2B, então..." ou "mas se for do canal mobile, então...". Isso indica que o que parece um contexto único é na verdade dois contextos com vocabulários e regras distintas sendo forçados a coexistir.
Exemplos de Context Map em código
// Bounded Context: Orders — o modelo de "Produto" em Orders
namespace Orders.Domain
{
// Em Orders, Produto é apenas uma referência com preço confirmado
public record ProductSnapshot(
string ProductId,
string Name,
Money ConfirmedPrice // preço no momento do pedido — imutável
);
public class OrderLine
{
public ProductSnapshot Product { get; private set; }
public int Quantity { get; private set; }
public Money Subtotal => new(Product.ConfirmedPrice.Amount * Quantity, Product.ConfirmedPrice.Currency);
}
}
// Bounded Context: Catalog — o modelo de "Produto" em Catalog (diferente!)
namespace Catalog.Domain
{
public class Product : AggregateRoot<ProductId>
{
public string Name { get; private set; }
public string Description { get; private set; }
public Money BasePrice { get; private set; }
public IReadOnlyList<ProductVariant> Variants { get; private set; }
public Weight Weight { get; private set; } // relevante para Catalog, irrelevante para Orders
public Dimensions Dimensions { get; private set; }
// ... muitos outros atributos que Orders não precisa
}
}
// Anti-Corruption Layer em Orders — traduz Catalog.Product para ProductSnapshot
namespace Orders.Infrastructure.Catalog
{
public class CatalogAcl : ICatalogModule
{
private readonly ICatalogHttpClient _catalog;
public async Task<ProductSnapshot?> GetProductSnapshotAsync(string productId)
{
var dto = await _catalog.GetProductAsync(productId); // DTO do Catalog
if (dto is null) return null;
// Traduz vocabulário do Catalog para vocabulário de Orders
return new ProductSnapshot(
ProductId: dto.Id,
Name: dto.Title, // "Title" em Catalog = "Name" em Orders
ConfirmedPrice: new Money(dto.SalePrice, dto.Currency) // preço de venda, não base
);
}
}
}
// Interface do módulo — Open Host Service com contrato explícito
namespace Catalog.Api
{
public interface ICatalogModule // Published Language
{
Task<ProductDto?> GetProductAsync(string productId, CancellationToken ct = default);
Task<bool> CheckStockAsync(string productId, int qty, CancellationToken ct = default);
}
}
from dataclasses import dataclass
from decimal import Decimal
# Bounded Context: Orders — "produto" em Orders
@dataclass(frozen=True)
class ProductSnapshot:
"""Em Orders, produto é apenas nome + preço confirmado no momento do pedido."""
product_id: str
name: str
confirmed_price: Decimal
currency: str
# Bounded Context: Catalog — "produto" em Catalog (muito mais rico)
@dataclass
class Product:
"""Em Catalog, produto tem atributos físicos, variantes, promoções, SEO..."""
id: str
title: str # "título" no Catalog = "nome" em Orders
description: str
base_price: Decimal
sale_price: Decimal # preço de venda (pode diferir da base)
currency: str
weight_kg: float # irrelevante para Orders
stock_qty: int
# Anti-Corruption Layer — traduz Catalog → vocabulário de Orders
class CatalogAcl:
def __init__(self, catalog_client: "CatalogHttpClient"):
self._client = catalog_client
async def get_product_snapshot(self, product_id: str) -> ProductSnapshot | None:
dto = await self._client.get_product(product_id) # modelo do Catalog
if not dto:
return None
# Tradução explícita: "title" → "name", "sale_price" → "confirmed_price"
return ProductSnapshot(
product_id=dto["id"],
name=dto["title"], # vocabulário diferente
confirmed_price=Decimal(str(dto["sale_price"])),
currency=dto["currency"],
)
# Published Language — contrato explícito do Catalog como Open Host
from pydantic import BaseModel
class ProductResponse(BaseModel): # schema publicado do Catalog
id: str
title: str
sale_price: float
currency: str
stock_qty: int
# Orders usa CatalogAcl, nunca ProductResponse diretamente
# Isso garante que mudanças no schema do Catalog não propagam para Orders
// Bounded Context: orders/domain/product_snapshot.go
package domain
// Em Orders, "produto" é snapshot imutável do momento do pedido
type ProductSnapshot struct {
ProductID string
Name string // "Name" em Orders = "Title" no Catalog
ConfirmedPrice float64
Currency string
}
// Bounded Context: catalog/domain/product.go
package domain
// Em Catalog, "produto" tem muitos mais atributos
type Product struct {
ID string
Title string // "Title" em Catalog = "Name" em Orders
SalePrice float64
BasePrice float64
WeightKg float64
StockQty int
// muitos outros campos relevantes para Catalog mas não para Orders
}
// Anti-Corruption Layer em Orders: orders/infra/catalog_acl.go
package infra
// CatalogACL traduz o modelo do Catalog para o vocabulário de Orders
type CatalogACL struct{ client CatalogHTTPClient }
func (a *CatalogACL) GetProductSnapshot(ctx context.Context, id string) (*domain.ProductSnapshot, error) {
dto, err := a.client.GetProduct(ctx, id) // DTO no formato do Catalog
if err != nil { return nil, err }
if dto == nil { return nil, nil }
return &domain.ProductSnapshot{
ProductID: dto.ID,
Name: dto.Title, // tradução de vocabulário
ConfirmedPrice: dto.SalePrice, // preço de venda, não base
Currency: dto.Currency,
}, nil
}
// Interface do módulo Catalog como Open Host Service
// catalog/api/module.go
type Module interface {
GetProduct(ctx context.Context, id string) (*ProductDTO, error)
CheckStock(ctx context.Context, id string, qty int) (bool, error)
}
// Orders depende de catalog.Module (interface), nunca de catalog.Product diretamente
// Isso é o Context Map em código: Orders ← (ACL) ← Catalog (Customer/Supplier)
Domain Vision Statement
Antes de modelar contextos, é útil escrever um Domain Vision Statement: um parágrafo curto que descreve o domínio principal do sistema, o que o diferencia de sistemas similares, e o que é central vs. o que é suporte. Esse statement guia quais partes do sistema merecem investimento em modelagem de domínio rica (o Core Domain) e quais podem ser compradas ou simplificadas (Supporting e Generic Subdomains).
A distinção de Evans entre Core Domain, Supporting Subdomain, e Generic Subdomain é estratégica: você investe a maior parte do esforço de engenharia no Core Domain (onde está o diferencial competitivo), usa soluções genéricas para Generic Subdomains (email, pagamento, autenticação), e implementa Supporting Subdomains de forma simples.
Referências
- Evans, E. — Domain-Driven Design. Addison-Wesley, 2003. Parte IV: Strategic Design — o texto canônico.
- Vernon, V. — Implementing Domain-Driven Design. Addison-Wesley, 2013. Caps 2-3: Bounded Context e Context Maps.
- Brandolini, A. — Introducing Event Storming. Leanpub, 2021. A técnica de descoberta de Bounded Contexts.
- Vernon, V. — Domain-Driven Design Distilled. Addison-Wesley, 2016. Versão condensada focada em Strategic Design.
- Millett, S.; Tune, N. — Patterns, Principles, and Practices of DDD. Wrox, 2015. Caps 5-8: Strategic DDD.
- Newman, S. — Building Microservices, 2ª ed. O'Reilly, 2021. Como Bounded Contexts mapeiam para serviços.
- Tune, N.; Millett, S. — "Context Mapping". ddd-crew.github.io. Guia visual de padrões de Context Map.
- Fowler, M. — "BoundedContext". martinfowler.com, 2014. A definição do padrão.
- Richardson, C. — Microservices Patterns. Manning, 2018. Caps 2-3: decomposição por Bounded Context.
- Hohpe, G.; Woolf, B. — Enterprise Integration Patterns. Addison-Wesley, 2003. Padrões de integração entre contextos.
- Evans, E. — "What I've learned about DDD since the book". QCon, 2009. A evolução do pensamento estratégico.
- Dahan, U. — "Strategic DDD". udidahan.com. Perspectiva prática sobre Core Domain.