Como aplicar DDD em projetos de médio porte sem over-engineering
1. Entendendo o escopo: DDD para médio porte vs. enterprise
Projetos de médio porte (5-15 desenvolvedores) frequentemente caem na armadilha de tentar replicar práticas de grandes corporações. A diferença fundamental não está na complexidade do negócio, mas na escala: enquanto sistemas enterprise precisam coordenar dezenas de times e contextos, projetos médios podem adotar uma abordagem mais enxuta.
Mitos comuns sobre DDD:
- DDD não exige microsserviços — você pode ter múltiplos Bounded Contexts em um único monólito modular
- Arquitetura hexagonal não é obrigatória — uma separação clara de camadas já resolve
- Event Sourcing e CQRS são ferramentas, não pré-requisitos
O princípio do “just enough DDD” significa aplicar apenas os padrões que trazem valor imediato. Se um conceito não reduz ruído de comunicação ou não simplifica a lógica de negócio, ele é over-engineering.
2. Delimitação de contextos com bom senso
Identificar Bounded Contexts exige critérios práticos:
- Equipe: cada contexto deve caber na cabeça de 2-3 desenvolvedores
- Linguagem ubíqua: termos diferentes para o mesmo conceito indicam contextos separados
- Ciclo de vida: contextos com frequência de mudança distinta devem ser separados
Exemplo: e-commerce de médio porte
Catálogo (produtos, categorias, preços)
└── Shared Kernel: ID do produto
Pedidos (carrinho, checkout, histórico)
└── Customer/Supplier: Catálogo fornece dados de produto
Pagamento (transações, reembolsos, antifraude)
└── Customer/Supplier: Pedidos fornece valor e dados do cliente
Entrega (logística, rastreio, transportadoras)
└── Evento: PedidoConfirmado → inicia processo de entrega
Apenas 4 contextos, com mapeamento simplificado. Nada de camadas de domínio compartilhado complexas ou relacionamentos OHS/PL.
3. Linguagem ubíqua prática e evolutiva
Construir a linguagem não precisa de semanas de reunião. Funciona melhor:
- Workshops curtos: 2-3 sessões de 1 hora com stakeholders e desenvolvedores
- Glossário vivo: um README no repositório principal, não um documento de 50 páginas
- Evolução natural: o termo muda conforme o entendimento amadurece
Exemplo de evolução:
Sprint 1: "item" → tudo que está no carrinho
Sprint 3: "produto em estoque" → item com quantidade disponível
Sprint 5: "produto em catálogo" → item exibido na loja (pode estar sem estoque)
A cada sprint, revise o glossário. Se alguém usa um termo diferente, investigue se é um novo conceito ou apenas variação linguística.
4. Modelagem tática: o essencial sem exageros
Entidades e Value Objects
Use Value Objects quando o conceito tem identidade baseada em seus atributos:
// Value Object: dinheiro (comparado por valor, não por ID)
class Dinheiro {
constructor(valor: number, moeda: string) {
if (valor < 0) throw new Error("Valor não pode ser negativo");
this.valor = valor;
this.moeda = moeda;
}
equals(outro: Dinheiro): boolean {
return this.valor === outro.valor && this.moeda === outro.moeda;
}
}
// Entidade: Pedido (identidade própria)
class Pedido {
constructor(id: string, clienteId: string) {
this.id = id; // Identidade única
this.clienteId = clienteId;
this.itens = [];
this.status = "rascunho";
}
adicionarItem(produto: Produto, quantidade: number) {
// Lógica de negócio aqui
}
}
Aggregate Rules
- 1 aggregate por transação: se você precisa salvar duas coisas juntas, provavelmente são o mesmo aggregate
- Limite de 5-7 entidades: acima disso, o aggregate fica difícil de gerenciar
- Consistência imediata: dentro do aggregate, tudo deve ser consistente; entre aggregates, consistência eventual
Repositórios e Domain Services
Repositórios apenas para aggregates raiz:
interface PedidoRepositorio {
buscarPorId(id: string): Pedido;
salvar(pedido: Pedido): void;
}
Domain Services para operações que não cabem em nenhuma entidade:
class CalculadoraDeFrete {
calcular(pedido: Pedido, cepDestino: string): Dinheiro {
// Lógica que envolve múltiplos aggregates
}
}
5. Estratégia de implementação sem frameworks pesados
A separação de camadas mínima:
src/
dominio/ # Entidades, Value Objects, Domain Services
aplicacao/ # Casos de uso (orquestração)
infraestrutura/ # Repositórios concretos, APIs externas
Exemplo de aggregate completo:
// domínio/Pedido.ts
class Pedido {
private itens: ItemPedido[] = [];
constructor(
readonly id: string,
readonly clienteId: string,
private repositorio: PedidoRepositorio
) {}
adicionarItem(produtoId: string, quantidade: number, precoUnitario: Dinheiro) {
if (this.status !== "rascunho") {
throw new Error("Só é possível adicionar itens em pedidos rascunho");
}
const item = new ItemPedido(produtoId, quantidade, precoUnitario);
this.itens.push(item);
}
confirmar() {
if (this.itens.length === 0) {
throw new Error("Pedido sem itens não pode ser confirmado");
}
this.status = "confirmado";
this.repositorio.salvar(this);
// Disparar evento de domínio (ver seção 6)
}
}
Nada de ORM complexo, repositórios genéricos ou injeção de dependência elaborada. Apenas classes com lógica de negócio clara.
6. Integração entre contextos sem acoplamento
Anti-corruption Layer (ACL) simplificado
Quando um contexto precisa se comunicar com um sistema legado:
// infraestrutura/adaptadores/CatalogoLegadoAdapter.ts
class CatalogoLegadoAdapter implements CatalogoPort {
buscarProduto(produtoId: string): Produto {
const resposta = await http.get(`/api/v1/produtos/${produtoId}`);
// Traduz do modelo legado para o modelo do domínio
return new Produto(
resposta.data.id,
resposta.data.nome,
new Dinheiro(resposta.data.preco, "BRL")
);
}
}
Eventos de domínio com mensageria básica
// domínio/eventos/PedidoConfirmado.ts
class PedidoConfirmado {
constructor(
readonly pedidoId: string,
readonly clienteId: string,
readonly total: Dinheiro,
readonly itens: ItemPedido[]
) {}
}
// aplicacao/ConfirmarPedidoUseCase.ts
class ConfirmarPedidoUseCase {
async executar(pedidoId: string) {
const pedido = await this.repositorio.buscarPorId(pedidoId);
pedido.confirmar();
// Publicar evento no barramento
await this.barramento.publicar(new PedidoConfirmado(
pedido.id,
pedido.clienteId,
pedido.calcularTotal(),
pedido.itens
));
}
}
O contexto de Pagamento escuta PedidoConfirmado e inicia o processamento. Sem Event Store, apenas RabbitMQ ou SQS.
7. Ciclo de feedback e refatoração contínua
Event Storming leve: 2 horas a cada sprint para validar:
- Os eventos de domínio ainda fazem sentido?
- Novos conceitos surgiram?
- Algum contexto está muito grande?
Indicadores de over-engineering:
- Repositórios genéricos com métodos que nunca são usados
- Aggregates com 1-2 entidades (provavelmente deveriam ser VO)
- Interfaces para tudo (se só tem uma implementação, talvez não precise)
- Camadas de aplicação que só delegam para o domínio
Checklist final para saber se o DDD está ajudando:
✅ A equipe usa a mesma linguagem para falar de negócio
✅ Mudanças de regra afetam poucos arquivos
✅ Novos desenvolvedores entendem o domínio em 1 semana
❌ Você tem mais de 5 contextos para um sistema de 8 pessoas
❌ A equipe gasta mais tempo configurando ferramentas do que modelando
❌ Existe um diagrama UML que ninguém atualiza há 3 meses
DDD de médio porte é sobre comunicação e clareza, não sobre arquitetura impressionante. Quando o time consegue mudar uma regra de negócio em um único aggregate sem quebrar outros contextos, você acertou.
Referências
- Domain-Driven Design: Tackling Complexity in the Heart of Software (Eric Evans) — Livro fundamental que introduz os conceitos de Bounded Context, Linguagem Ubíqua e Modelagem Tática
- Implementing Domain-Driven Design (Vaughn Vernon) — Guia prático com exemplos de implementação, incluindo estratégias para evitar over-engineering
- DDD Quickly (Abel Avram, Floyd Marinescu) — Resumo conciso dos padrões DDD, ideal para equipes que querem começar sem complexidade desnecessária
- Martin Fowler: BoundedContext — Artigo clássico explicando como delimitar contextos e quando evitar granularidade excessiva
- Event Storming (Alberto Brandolini) — Técnica de modelagem colaborativa que ajuda a identificar Bounded Contexts e eventos de domínio sem over-engineering
- DDD e Microserviços: Nem tudo que reluz é ouro (ThoughtWorks) — Reflexão sobre quando usar DDD com microsserviços e quando manter monólitos modulares
- Refactoring: Improving the Design of Existing Code (Martin Fowler) — Técnicas de refatoração contínua essenciais para evoluir o modelo de domínio sem dor