DDD sem a teoria toda de uma vez: introdução incremental
1. Por que começar pequeno com DDD?
O maior erro ao adotar Domain-Driven Design é tentar implementar todos os conceitos de uma só vez. Agregados complexos, eventos distribuídos, repositórios genéricos — isso gera paralisia e código superengenheirado. O caminho mais seguro é o incrementalismo: começar com um sistema CRUD simples e, a cada iteração, adicionar uma camada de riqueza ao modelo.
Exemplo de um sistema de pedidos inicial (código procedural):
public class PedidoService {
public void criarPedido(String clienteId, List<String> itens) {
// validação inline
if (clienteId == null || itens.isEmpty()) {
throw new IllegalArgumentException("Dados inválidos");
}
// lógica de negócio misturada com infraestrutura
Pedido pedido = new Pedido();
pedido.setClienteId(clienteId);
pedido.setItens(itens);
pedido.setStatus("CRIADO");
pedidoRepository.save(pedido);
}
}
Esse código funciona, mas não expressa o domínio. A cada iteração, vamos refiná-lo.
2. Identificando a linguagem ubíqua sem jargões
A linguagem ubíqua não surge de diagramas UML, mas de conversas com especialistas do negócio. Sente-se com um analista de vendas e anote os termos que ele usa naturalmente: "pedido", "item de linha", "cliente VIP", "prazo de entrega".
Glossário mínimo para começar:
| Termo do negócio | Significado |
|---|---|
| Pedido | Solicitação de compra com identificador único |
| Item de linha | Produto com quantidade e preço dentro de um pedido |
| Cliente | Pessoa ou empresa que realiza pedidos |
| Prazo de entrega | Data limite para envio do pedido |
Agora, renomeie classes e métodos:
// Antes: nomes genéricos
public class Order { ... }
public class Item { ... }
// Depois: nomes do domínio
public class Pedido { ... }
public class ItemDeLinha {
private String produtoId;
private int quantidade;
private double precoUnitario;
public double calcularSubtotal() {
return quantidade * precoUnitario;
}
}
3. Primeiros passos com Entidades e Value Objects
A diferença prática: Entidades têm identidade (ID único que persiste ao longo do tempo). Value Objects são intercambiáveis e definidos por seus atributos.
Comece transformando strings soltas em Value Objects tipados:
// Antes: string genérica
public class Pedido {
private String emailCliente;
}
// Depois: Value Object
public class Email {
private final String valor;
public Email(String valor) {
if (valor == null || !valor.contains("@")) {
throw new IllegalArgumentException("Email inválido");
}
this.valor = valor;
}
public String getValor() { return valor; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Email email = (Email) o;
return valor.equals(email.valor);
}
}
Agora, regras de negócio ficam encapsuladas no construtor. Se o email for inválido, a própria classe impede sua criação.
4. Agregados: o que agrupar e o que separar
A regra dos 80/20: agregados devem ser pequenos o suficiente para garantir consistência transacional, mas grandes o bastante para manter invariantes. Um erro comum é criar um agregado "Pedido" que contém tudo: cliente, itens, pagamento, entrega.
Exemplo de refatoração: separar um agregado gigante em dois coesos.
// Agregado original (inchado)
public class Pedido {
private List<ItemDeLinha> itens;
private Pagamento pagamento;
private EnderecoEntrega endereco;
private Cliente cliente;
}
// Após refatoração: dois agregados
public class Pedido {
private List<ItemDeLinha> itens; // invariante: total do pedido
private PedidoId id;
}
public class Pagamento {
private PagamentoId id;
private PedidoId pedidoId; // referência por ID, não por objeto
private double valor;
private StatusPagamento status;
}
A regra prática: se duas entidades mudam juntas na mesma transação, pertencem ao mesmo agregado. Caso contrário, separe-as.
5. Repositórios sem abstração excessiva
Repositórios devem parecer coleções do domínio. Comece com implementação concreta, sem abstrair prematuramente.
// Implementação concreta com JPA
public class PedidoRepositoryJpa {
@PersistenceContext
private EntityManager em;
public Pedido buscarPorId(PedidoId id) {
return em.find(Pedido.class, id.getValor());
}
public void salvar(Pedido pedido) {
em.persist(pedido);
}
}
Extraia uma interface apenas quando surgir uma segunda fonte de dados (ex.: cache Redis + banco relacional). Não crie IRepository<T> genérico — isso é infraestrutura, não domínio.
6. Serviços de domínio: o que não cabe nas entidades
Serviços de Domínio contêm lógica que não pertence naturalmente a uma única Entidade ou Value Object. Diferem de Serviços de Aplicação porque operam no domínio puro, sem dependências de infraestrutura.
Exemplo incremental: extrair lógica de uma entidade inchada.
// Entidade inchada
public class Pedido {
public double calcularFrete(String cepOrigem, String cepDestino) {
// lógica complexa de frete
}
}
// Serviço de domínio extraído
public class CalculadoraDeFrete {
public Frete calcular(Pedido pedido, String cepOrigem, String cepDestino) {
double valorBase = pedido.getPesoTotal() * 0.5;
double taxaDistancia = calcularTaxaPorDistancia(cepOrigem, cepDestino);
return new Frete(valorBase + taxaDistancia);
}
private double calcularTaxaPorDistancia(String origem, String destino) {
// lógica de distância
}
}
Mantenha o comportamento nas entidades sempre que possível. Extraia serviços apenas quando a lógica envolver múltiplas entidades ou regras externas.
7. Eventos de domínio como próximo passo
Eventos de domínio notificam que algo importante aconteceu. Comece com implementação leve — sem filas, apenas notificações síncronas.
// Evento simples
public class PedidoCriado {
private final PedidoId pedidoId;
private final Instant ocorridoEm;
public PedidoCriado(PedidoId pedidoId) {
this.pedidoId = pedidoId;
this.ocorridoEm = Instant.now();
}
}
// Publicação no agregado
public class Pedido {
private List<DomainEvent> eventos = new ArrayList<>();
public void criar(List<ItemDeLinha> itens) {
this.itens = itens;
this.status = StatusPedido.CRIADO;
eventos.add(new PedidoCriado(this.id));
}
public List<DomainEvent> getEventos() {
return Collections.unmodifiableList(eventos);
}
}
Outros agregados podem reagir: "Quando um pedido for criado, reserve o estoque." Isso reduz acoplamento.
8. Refatorando para DDD: um ciclo prático
Passo a passo em 3 iterações:
Iteração 1 — Código procedural:
- Classes anêmicas com getters/setters
- Lógica de negócio em serviços genéricos
- Validações espalhadas
Iteração 2 — Value Objects e Entidades:
- Extrair Email, CPF, Dinheiro como Value Objects
- Mover validações para construtores
- Renomear classes para linguagem ubíqua
Iteração 3 — Agregados e Eventos:
- Definir fronteiras de agregados
- Extrair Serviços de Domínio
- Introduzir eventos para comunicação entre agregados
Métricas de sucesso:
- Redução de 40% em if-else aninhados
- Aumento de legibilidade: métodos com 5-10 linhas
- Testes unitários focados em regras de negócio
Checklist do que evitar:
- Over-engineering: não crie fábricas, specifications ou camadas extras sem necessidade
- Anêmicos: cada entidade deve ter comportamento, não apenas dados
- Abstração prematura: interfaces só quando houver múltiplas implementações
Priorize valor de negócio: pergunte-se "essa mudança torna o código mais alinhado com o domínio?" Se a resposta for não, adie.
Referências
- Domain-Driven Design: Tackling Complexity in the Heart of Software (Eric Evans) — Livro fundamental que introduziu os conceitos de DDD, linguagem ubíqua e agregados
- Implementing Domain-Driven Design (Vaughn Vernon) — Guia prático com exemplos de implementação incremental de DDD
- Martin Fowler — AnemicDomainModel — Artigo que explica o anti-padrão de modelos anêmicos e como evitá-lo
- DDD Quickly (InfoQ) — Resumo acessível dos conceitos de DDD para iniciantes
- Refactoring to Domain-Driven Design (Vladimir Khorikov) — Série de artigos sobre refatoração incremental para DDD, com exemplos de código