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