Como modelar agregados no DDD sem over-engineering
1. Entendendo o que realmente é um Agregado
No Domain-Driven Design, o agregado é uma unidade de consistência transacional. Isso significa que todas as operações que modificam o estado interno de um agregado devem ser atômicas — ou tudo é salvo, ou nada é salvo. A fronteira do agregado define exatamente quais objetos compartilham essa garantia.
O mito mais comum é pensar que o agregado precisa ser uma estrutura enorme que contém tudo relacionado ao conceito. Na prática, menos é mais. Cada agregado deve proteger um conjunto mínimo de invariantes de negócio. Se você pode quebrar um agregado em partes menores sem violar regras de consistência, faça isso.
Exemplo prático: Um sistema de e-commerce pode ter um agregado Pedido que contém itens, mas não precisa conter o histórico completo do cliente. O cliente é outro agregado.
// Agregado Pedido (bem modelado)
class Pedido {
id: PedidoId
clienteId: ClienteId // referência por ID, não o objeto Cliente inteiro
itens: List<ItemPedido>
status: StatusPedido
total: Monetario
fun adicionarItem(produtoId: ProdutoId, quantidade: Int) {
// invariante: não pode adicionar item em pedido fechado
if (status == StatusPedido.FECHADO) throw Erro("Pedido já fechado")
itens.add(ItemPedido(produtoId, quantidade))
recalcularTotal()
}
fun fechar() {
if (itens.isEmpty()) throw Erro("Pedido sem itens não pode ser fechado")
status = StatusPedido.FECHADO
}
private fun recalcularTotal() {
total = itens.somatorio { it.preco * it.quantidade }
}
}
2. Identificando a raiz do agregado (Aggregate Root)
A raiz do agregado é a única entidade que pode ser referenciada externamente. Ela possui identidade própria, controla o ciclo de vida do agregado e é a porta de entrada para todas as operações.
Critérios para escolher a raiz:
- Identidade única: a raiz deve ter um identificador que faça sentido no domínio
- Ciclo de vida: a raiz é responsável por criar e destruir as entidades internas
- Acesso externo: apenas a raiz pode ser obtida por repositórios
Evite criar raízes artificiais. Muitas vezes, uma value object ou uma entidade simples já resolve o problema. Por exemplo, um Endereco pode ser uma value object dentro de Cliente, não precisa ser um agregado próprio.
Erro comum: usar Cliente como raiz de tudo, incluindo pedidos. Na verdade, Pedido é a raiz do agregado de pedidos, e Cliente é a raiz do agregado de clientes.
// Agregado Cliente (raiz correta)
class Cliente {
id: ClienteId
nome: String
endereco: Endereco // value object, não agregado separado
email: Email
fun alterarEndereco(novoEndereco: Endereco) {
endereco = novoEndereco
}
}
// Agregado Pedido (raiz separada)
class Pedido {
id: PedidoId
clienteId: ClienteId // apenas referência
// ...
}
3. Regras de consistência: o ponto de equilíbrio
Dentro de um agregado, a consistência é imediata. Entre agregados, a consistência pode ser eventual. Esse é o ponto mais crítico para evitar over-engineering.
Quando quebrar um agregado? Quando o custo da consistência forte supera os benefícios. Por exemplo, em um sistema de reservas, ter todos os assentos de um voo em um único agregado causa contenção de concorrência. Melhor ter cada assento como agregado independente.
Regras invariantes: proteja apenas o mínimo necessário para o negócio. Se uma regra pode ser violada por alguns segundos sem prejuízo real, ela não precisa estar dentro do agregado.
// Agregado pequeno: Reserva de assento individual
class ReservaAssento {
id: ReservaAssentoId
vooId: VooId
assento: String
status: StatusReserva // DISPONIVEL, RESERVADO, CONFIRMADO
fun reservar(clienteId: ClienteId) {
if (status != StatusReserva.DISPONIVEL) throw Erro("Assento ocupado")
status = StatusReserva.RESERVADO
// consistência eventual: confirmação pode vir depois
}
}
4. Tamanho ideal do agregado: nem pequeno demais, nem grande demais
O agregado anêmico não tem regras de negócio — é apenas um container de dados. Isso fere o DDD. Já o agregado Deus contém tudo e causa acoplamento excessivo.
Uma heurística prática: comece com 3 a 5 entidades por agregado. Isso força a reflexão sobre o que realmente precisa estar junto.
Sinais de que o agregado está grande demais:
- Múltiplos desenvolvedores modificam o mesmo agregado simultaneamente
- Testes de concorrência falham com frequência
- Operações de carregamento são lentas
// Agregado grande demais (evitar)
class SistemaCompleto {
clientes: List<Cliente>
pedidos: List<Pedido>
produtos: List<Produto>
estoque: Map<ProdutoId, Int>
// violação: tudo junto, sem fronteiras claras
}
// Agregado anêmico (evitar)
class PedidoAnemico {
id: Int
clienteId: Int
itens: List<ItemPedidoDTO> // sem comportamento
// regras de negócio espalhadas em serviços
}
5. Como modelar relacionamentos entre agregados
Use referências por identidade (IDs) em vez de objetos inteiros. Isso reduz o acoplamento e permite que cada agregado seja carregado independentemente.
Serviços de domínio orquestram operações que envolvem múltiplos agregados. Eles não contêm regras de negócio, apenas coordenam a execução.
Eventos de domínio são o mecanismo ideal para comunicação assíncrona entre agregados. Quando um Pedido é fechado, um evento PedidoFechado pode disparar a atualização do estoque em outro agregado.
// Serviço de domínio coordenando agregados
class ServicoFechamentoPedido {
fun fecharPedido(pedidoId: PedidoId): PedidoFechado {
val pedido = repositorioPedido.buscar(pedidoId)
pedido.fechar() // valida regras internas
repositorioPedido.salvar(pedido)
// dispara evento para outros agregados
return PedidoFechado(pedidoId, pedido.clienteId, pedido.total)
}
}
// Evento de domínio
class PedidoFechado(
val pedidoId: PedidoId,
val clienteId: ClienteId,
val total: Monetario
)
6. Armadilhas comuns de over-engineering em agregados
Hierarquias profundas de herança: raramente são necessárias. Prefira composição.
Padrões complexos antes da necessidade: Specification, Visitor, Strategy — use apenas quando o código atual claramente pede por eles.
Testes excessivos: testar cenários que nunca ocorrerão no negócio real. Foque nos invariantes.
// Over-engineering: herança desnecessária
class EntidadeBase { ... }
class Pessoa : EntidadeBase { ... }
class Cliente : Pessoa { ... }
class ClienteVIP : Cliente { ... }
class ClienteVIPInternacional : ClienteVIP { ... }
// 4 níveis de herança para algo que poderia ser um atributo
// Melhor: composição e atributos simples
class Cliente {
tipo: TipoCliente // enum: COMUM, VIP, VIP_INTERNACIONAL
// comportamento baseado no tipo, não em herança
}
7. Estratégias práticas para evoluir agregados sem refatoração traumática
Comece maior e refine: é mais fácil quebrar um agregado grande do que juntar vários pequenos depois. Modele com um agregado um pouco maior e, conforme a demanda, divida.
Use bounded contexts: se um agregado cresce demais, talvez ele pertença a outro contexto. Separe por contexto delimitado.
Diagramas simples: desenhe as fronteiras dos agregados em um quadro branco. Valide com a equipe de negócio antes de codificar.
// Exemplo de evolução: de agregado único para múltiplos
// Versão 1: agregado grande (ok para começar)
class Pedido {
itens: List<ItemPedido>
pagamento: Pagamento
entrega: Entrega
}
// Versão 2: após perceber que Pagamento tem regras próprias
class Pedido {
itens: List<ItemPedido>
pagamentoId: PagamentoId // referência por ID
entregaId: EntregaId
}
// Versão 3: Pagamento e Entrega como agregados independentes
class Pagamento {
id: PagamentoId
pedidoId: PedidoId
status: StatusPagamento
valor: Monetario
}
Modelar agregados no DDD sem over-engineering é uma questão de equilíbrio. Comece pequeno, mas não anêmico. Proteja os invariantes reais do negócio, não os imaginários. Use referências por ID, eventos de domínio e serviços de coordenação. E, acima de tudo, lembre-se: o agregado é uma ferramenta para gerenciar complexidade, não para aumentá-la.
Referências
- Martin Fowler — Aggregate Pattern — Descrição clássica do padrão Aggregate no DDD, com exemplos conceituais.
- Vaughn Vernon — Effective Aggregate Design — Artigo seminal sobre design de agregados, com recomendações práticas de tamanho e consistência.
- Microsoft — DDD: Aggregate Design Guidelines — Guia oficial da Microsoft para modelagem de agregados em microsserviços.
- DDD Community — Aggregates in Domain-Driven Design — Compilação de recursos e discussões da comunidade sobre agregados.
- Udi Dahan — Race Conditions in Aggregates — Análise prática sobre concorrência e consistência em agregados, com exemplos reais.