Domain-Driven Design (DDD)
Como aplicar DDD para modelar domínios complexos, definir bounded contexts e construir software alinhado com o negócio.
Que problema resolve
Em sistemas complexos, o maior desafio não é técnico, mas sim comunicacional. Os desenvolvedores falam um idioma, os especialistas de negócio falam outro, e o software acaba sendo uma tradução defeituosa de ambos. O resultado: modelos de dados que não refletem a realidade do negócio, lógica espalhada em camadas incorretas e mudanças que exigem modificar meio sistema.
Domain-Driven Design ataca esse problema pela raiz: propõe que o software deve modelar diretamente o domínio do negócio, usando a mesma linguagem que os especialistas do domínio usam. Não é um framework nem uma biblioteca, é uma abordagem de design que coloca o domínio no centro de todas as decisões arquiteturais.
Sintomas de que você precisa de DDD
- A equipe técnica e a equipe de negócio não se entendem
- Os modelos de dados são tabelas de banco de dados disfarçadas de objetos
- Uma mudança em uma regra de negócio exige modificar múltiplos serviços
- Não há limites claros entre as responsabilidades de cada parte do sistema
- A lógica de negócio está espalhada entre controllers, services e repositories
- Os nomes no código não coincidem com os termos que o negócio usa
Exemplo concreto do problema
Imagine um sistema de e-commerce onde a equipe de negócio fala de “pedidos”, “devoluções” e “reembolsos”, mas o código tem classes como DataProcessor, ItemHandler e TransactionManager. Quando o negócio diz “precisamos mudar a política de devoluções”, ninguém sabe quais arquivos mexer, porque não existe uma classe Devolução nem um módulo Reembolsos.
❌ Sin DDD — Código desconectado del negocio:
src/
├── handlers/
│ ├── DataProcessor.java
│ ├── ItemHandler.java
│ └── TransactionManager.java
├── models/
│ ├── Record.java
│ └── Entry.java
└── utils/
└── Helper.java
✅ Con DDD — Código alineado con el dominio:
src/
├── pedidos/
│ ├── Pedido.java
│ ├── LíneaDePedido.java
│ └── RepositorioPedidos.java
├── devoluciones/
│ ├── Devolución.java
│ ├── PolíticaDevolución.java
│ └── ServicioReembolso.java
└── envíos/
├── Envío.java
└── TransportistaAdapter.java
Como funciona
O DDD se divide em duas grandes áreas: design estratégico (como dividir o sistema) e design tático (como modelar cada parte).
Design estratégico
Ubiquitous Language (Linguagem Ubíqua)
É o vocabulário compartilhado entre desenvolvedores e especialistas do domínio. Se o negócio fala de “pedidos”, “envios” e “devoluções”, o código deve usar exatamente esses termos — e não “orders_table”, “shipping_record” ou “return_entity”.
❌ Código sin lenguaje ubicuo:
class DataRecord { processItem() }
✅ Código con lenguaje ubicuo:
class Pedido { confirmarEnvío() }
A linguagem ubíqua é documentada, mantida e usada em conversas, código, testes e documentação. Não é apenas uma convenção de nomes — é um acordo ativo entre negócio e desenvolvimento que evolui junto com o domínio.
Como construir a linguagem ubíqua:
- Organize sessões de Event Storming com especialistas do domínio e desenvolvedores
- Identifique os termos-chave que o negócio usa e documente-os em um glossário compartilhado
- Use esses termos exatos no código, nos testes, na documentação e nas conversas
- Quando surgir ambiguidade (a mesma palavra significando coisas diferentes), é sinal de que você precisa de bounded contexts separados
Bounded Contexts (Contextos Delimitados)
Um bounded context é um limite explícito dentro do qual um modelo de domínio tem significado consistente. A mesma palavra pode significar coisas diferentes em contextos distintos:
- “Cliente” no contexto de Vendas: alguém que compra produtos
- “Cliente” no contexto de Suporte: alguém que abre tickets
- “Cliente” no contexto de Faturamento: uma entidade com dados fiscais
┌─────────────────────┐ ┌─────────────────────┐
│ Contexto: Ventas │ │ Contexto: Soporte │
│ │ │ │
│ Cliente │ │ Cliente │
│ ├─ nombre │ │ ├─ nombre │
│ ├─ historialCompra │ │ ├─ ticketsAbiertos │
│ └─ descuento │ │ └─ nivelPrioridad │
│ │ │ │
│ Pedido │ │ Ticket │
│ ├─ líneas │ │ ├─ descripción │
│ └─ total │ │ └─ estado │
└─────────────────────┘ └─────────────────────┘
Cada bounded context tem seu próprio modelo, seu próprio banco de dados e sua própria equipe (idealmente). A comunicação entre contextos é feita por meio de interfaces bem definidas.
Regras práticas para identificar bounded contexts:
- Se duas equipes precisam de modelos diferentes do mesmo conceito → contextos separados
- Se uma mudança em uma área não deveria afetar outra → contextos separados
- Se a linguagem ubíqua muda → você está cruzando um limite de contexto
- Um bounded context não é necessariamente um microsserviço, mas é um candidato natural a se tornar um
Context Mapping
Define como os bounded contexts se relacionam entre si:
| Relação | Descrição | Exemplo |
|---|---|---|
| Shared Kernel | Dois contextos compartilham um subconjunto do modelo | Tipos de moeda compartilhados entre Vendas e Faturamento |
| Customer-Supplier | Um contexto fornece dados que outro consome | Estoque (supplier) fornece stock a Pedidos (customer) |
| Conformist | Um contexto se adapta ao modelo de outro sem negociação | Seu sistema se adapta ao modelo da API de um fornecedor externo |
| Anti-Corruption Layer | Um contexto traduz o modelo externo para sua própria linguagem | Adapter que traduz respostas de um sistema legado |
| Open Host Service | Um contexto expõe um protocolo aberto para que outros o consumam | API REST pública com documentação OpenAPI |
| Published Language | Usa-se uma linguagem compartilhada documentada (JSON Schema, Protobuf) | Contratos de eventos no formato Avro |
Design tático
Entities (Entidades)
Objetos com identidade única que persiste ao longo do tempo. Duas entidades com os mesmos atributos, mas com IDs diferentes, são diferentes.
Pedido #1234 ≠ Pedido #5678
(aunque ambos tengan los mismos productos y el mismo total)
As entidades encapsulam comportamento, não apenas dados. Um Pedido não é um DTO com getters e setters — tem métodos que refletem operações do domínio:
// ❌ Modelo anémico (anti-patrón)
pedido.setEstado("confirmado");
pedido.setFechaConfirmacion(new Date());
// ✅ Modelo rico con comportamiento
pedido.confirmar(); // internamente valida reglas y cambia estado
Value Objects (Objetos de Valor)
Objetos definidos por seus atributos, sem identidade própria. Dois value objects com os mesmos atributos são iguais. São imutáveis.
Dirección("Calle 1", "Ciudad A") == Dirección("Calle 1", "Ciudad A")
Dinero(100, "USD") == Dinero(100, "USD")
Os value objects são mais comuns do que parece. Muitos conceitos que modelamos como strings ou números primitivos deveriam ser value objects:
// ❌ Tipos primitivos
String email = "user@example.com";
double precio = 99.99;
// ✅ Value objects con validación
Email email = new Email("user@example.com"); // valida formato
Dinero precio = new Dinero(99.99, "USD"); // evita mezclar monedas
Aggregates (Agregados)
Um cluster de entidades e value objects que são tratados como uma unidade de consistência. Cada agregado tem uma raiz (Aggregate Root), que é o único ponto de acesso externo.
┌─────────────────────────────┐
│ Aggregate: Pedido │
│ ┌───────────────────────┐ │
│ │ Pedido (Aggregate Root)│ │
│ │ ├─ id │ │
│ │ ├─ estado │ │
│ │ └─ fechaCreación │ │
│ └───────────────────────┘ │
│ ┌──────────┐ ┌──────────┐ │
│ │ LíneaPed │ │ LíneaPed │ │
│ │ producto │ │ producto │ │
│ │ cantidad │ │ cantidad │ │
│ │ precio │ │ precio │ │
│ └──────────┘ └──────────┘ │
│ ┌──────────────────────┐ │
│ │ DirecciónEnvío (VO) │ │
│ └──────────────────────┘ │
└─────────────────────────────┘
Regras-chave dos agregados:
- As transações só devem modificar um agregado por vez
- As referências entre agregados são feitas por ID, não por referência direta
- Se você precisa modificar vários agregados, use Domain Events
- Mantenha os agregados pequenos — um agregado grande é sinal de limites incorretos
Domain Events (Eventos de Domínio)
Representam algo que aconteceu no domínio. São expressos no passado e na linguagem do negócio.
PedidoConfirmado { pedidoId, fecha, total }
PagoRecibido { pagoId, pedidoId, monto }
EnvíoDespachado { envíoId, pedidoId, transportista }
Os eventos permitem que os bounded contexts se comuniquem de forma desacoplada. São a base da Event-Driven Architecture e do Saga Pattern para coordenar transações distribuídas.
Exemplo de fluxo com eventos:
Contexto Ventas:
pedido.confirmar()
→ publica PedidoConfirmado
Contexto Inventario:
escucha PedidoConfirmado
→ reserva stock
→ publica StockReservado
Contexto Envíos:
escucha StockReservado
→ programa envío
→ publica EnvíoProgramado
Repositories (Repositórios)
Abstrações que encapsulam o acesso a dados. O domínio não sabe se os dados vêm de um banco de dados, de uma API ou de um arquivo. O repositório fala a linguagem do domínio.
❌ pedidoDAO.selectByStatusAndDate(status, date)
✅ repositorioPedidos.buscarPendientesDesde(fecha)
Domain Services (Serviços de Domínio)
Operações que não pertencem naturalmente a nenhuma entidade ou value object. Contêm lógica de negócio que envolve múltiplos agregados.
ServicioDePrecios.calcularDescuento(cliente, pedido, promoción)
ServicioDeEnvío.seleccionarTransportista(pedido, destino)
Exemplo prático: E-commerce com DDD
Vejamos como o DDD é aplicado a um sistema de e-commerce real:
Passo 1: Identificar bounded contexts
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Catálogo │ │ Pedidos │ │ Pagos │
│ │ │ │ │ │
│ Producto │ │ Pedido │ │ Transacción │
│ Categoría │ │ LíneaDePedido │ │ MétodoDePago │
│ Precio │ │ DirecciónEnvío │ │ Reembolso │
│ Inventario │ │ EstadoPedido │ │ EstadoPago │
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘
│ │ │
│ Eventos │ Eventos │
└────────────────────┴────────────────────┘
Passo 2: Definir agregados no contexto de Pedidos
Aggregate Root: Pedido
├── id: PedidoId (Value Object)
├── cliente: ClienteId (referencia por ID, no por objeto)
├── líneas: List<LíneaDePedido>
│ ├── productoId: ProductoId
│ ├── cantidad: Cantidad (Value Object, > 0)
│ └── precioUnitario: Dinero (Value Object)
├── dirección: DirecciónEnvío (Value Object)
├── estado: EstadoPedido (enum)
└── total(): Dinero (calculado)
Invariantes del agregado:
- Un pedido debe tener al menos una línea
- El total nunca puede ser negativo
- Solo se puede confirmar un pedido en estado "borrador"
- Solo se puede cancelar un pedido que no ha sido enviado
Passo 3: Implementar comportamento no agregado
class Pedido {
confirmar() {
if (this.estado !== 'borrador')
throw new Error('Solo se puede confirmar un pedido en borrador');
if (this.líneas.length === 0)
throw new Error('El pedido debe tener al menos una línea');
this.estado = 'confirmado';
this.fechaConfirmación = new Date();
this.registrarEvento(new PedidoConfirmado(this.id, this.total()));
}
cancelar(motivo) {
if (this.estado === 'enviado' || this.estado === 'entregado')
throw new Error('No se puede cancelar un pedido ya enviado');
this.estado = 'cancelado';
this.motivoCancelación = motivo;
this.registrarEvento(new PedidoCancelado(this.id, motivo));
}
}
Vantagens
- Alinhamento negócio-tecnologia: O código reflete diretamente o domínio, reduzindo a lacuna de comunicação
- Limites claros: Os bounded contexts definem fronteiras explícitas que facilitam a evolução independente
- Modelo rico: As entidades contêm comportamento, não apenas dados, evitando modelos anêmicos
- Escalabilidade organizacional: Cada bounded context pode ser desenvolvido por uma equipe independente
- Evolução controlada: As mudanças em um contexto não afetam os outros se as interfaces forem mantidas
- Base para microsserviços: Os bounded contexts são candidatos naturais a se tornarem microsserviços
- Testes mais claros: Os testes refletem regras de negócio reais, e não implementações técnicas
Trade-offs / Desvantagens
- Curva de aprendizado alta: O DDD introduz muitos conceitos que exigem tempo para dominar
- Overhead em domínios simples: Para CRUDs básicos, o DDD adiciona complexidade desnecessária
- Requer acesso a especialistas do domínio: Sem colaboração constante com o negócio, o modelo se desvia
- Mais código inicial: Entidades, value objects, repositórios e eventos exigem mais código do que uma abordagem direta
- Risco de superengenharia: É fácil aplicar todos os padrões táticos quando você só precisa de alguns
- Duplicação intencional: Cada bounded context pode ter sua própria representação de conceitos similares
- Investimento em descoberta: As sessões de Event Storming e modelagem exigem tempo e participação ativa do negócio
Quando usar
- Domínios complexos com muitas regras de negócio
- Sistemas que evoluem com frequência devido a mudanças no negócio
- Equipes grandes que precisam trabalhar em paralelo com limites claros
- Projetos onde a comunicação entre negócio e desenvolvimento é crítica
- Sistemas que estão sendo migrados de monólito para microsserviços
- Quando o custo de um modelo incorreto é alto (finanças, saúde, logística)
Quando evitar
- Aplicações CRUD simples sem lógica de negócio significativa
- Protótipos ou MVPs onde a velocidade é mais importante que a estrutura
- Equipes pequenas sem acesso a especialistas do domínio
- Sistemas com domínios bem compreendidos e estáveis que não mudam
- Projetos com orçamento ou tempo muito limitado para a fase de design
Tecnologias e implementações comuns
| Categoria | Opções |
|---|---|
| Frameworks DDD | Axon Framework (Java), EventSourcing.NetCore (C#), Ecotone (PHP) |
| Event Sourcing | EventStoreDB, Axon Server, Marten (.NET) |
| CQRS | MediatR (.NET), Axon (Java), custom implementations |
| Mensageria | Apache Kafka, RabbitMQ, Amazon EventBridge |
| Modelagem | Event Storming, Domain Storytelling, Context Mapping |
| Persistência | PostgreSQL, MongoDB (por bounded context) |
Relação com outros padrões
O DDD é a fundação conceitual sobre a qual muitos outros padrões de arquitetura distribuída são construídos:
┌─────────────────────────────────────────────────┐
│ Domain-Driven Design │
│ (Bounded Contexts + Ubiquitous Language) │
└──────────┬──────────┬──────────┬────────────────┘
│ │ │
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────────┐
│Microserv.│ │ Event │ │ Shared │
│ Pattern │ │ Driven │ │ Kernel │
└──────────┘ └──────────┘ └──────────────┘
│ │
▼ ▼
┌──────────┐ ┌──────────┐
│ Saga │ │ Outbox │
│ Pattern │ │ Pattern │
└──────────┘ └──────────┘
- Microservices Pattern: Os bounded contexts do DDD são o guia para definir os limites de cada microsserviço
- Event-Driven Architecture: Os Domain Events do DDD são a base da comunicação assíncrona entre serviços
- Saga Pattern: Coordena transações que cruzam múltiplos bounded contexts usando eventos e compensações
- Outbox Pattern: Garante que os Domain Events sejam publicados de forma confiável junto com as mudanças de dados
- Anti-Corruption Layer (ACL): Implementa a relação de Context Mapping que protege seu domínio de modelos externos
- Shared Kernel: Implementa a relação de Context Mapping onde dois contextos compartilham um subconjunto do modelo
- API Gateway / BFF: Fornecem pontos de entrada que podem mapear requests externos para os bounded contexts internos
Erros comuns ao aplicar DDD
| Erro | Consequência | Solução |
|---|---|---|
| Aplicar DDD a todo o sistema | Superengenharia em áreas simples | Use DDD apenas no core domain, CRUD para o resto |
| Bounded contexts grandes demais | Monólito distribuído | Divida por subdomínios reais, não por camadas técnicas |
| Bounded contexts pequenos demais | Serviços tagarelas | Agrupe funcionalidades que mudam juntas |
| Ignorar a linguagem ubíqua | Código desconectado do negócio | Sessões regulares com especialistas do domínio |
| Modelo anêmico | Lógica espalhada em services | Mova o comportamento para as entidades |
| Agregados grandes demais | Problemas de concorrência | Mantenha os agregados pequenos, use eventos entre eles |
Próximos passos
Com o DDD como base para modelar domínios complexos, o próximo passo natural é entender como lidar com transações que cruzam múltiplos bounded contexts. Para isso existe o Saga Pattern, que coordena operações distribuídas com compensações. Explore também o Microservices Pattern para entender como os bounded contexts se transformam em serviços independentes.