Domain-Driven Design: introdução e aplicação prática
1. Fundamentos do Domain-Driven Design
Domain-Driven Design (DDD) é uma abordagem de desenvolvimento de software que coloca o domínio do negócio no centro do processo de design. Surgiu como resposta ao problema da complexidade acidental — aquela que criamos ao modelar soluções técnicas que não refletem fielmente a realidade do negócio — em oposição à complexidade essencial, que é inerente ao próprio domínio.
Os três pilares fundamentais do DDD são:
- Ubiquitous Language: uma linguagem compartilhada entre desenvolvedores e especialistas de negócio
- Bounded Context: limites explícitos que definem onde um modelo é válido
- Centralidade do domínio: o código deve refletir o modelo de negócio, não detalhes técnicos
DDD é mais indicado para projetos com lógica de negócio rica, regras complexas e equipes multidisciplinares que precisam colaborar intensamente. Para aplicações CRUD simples, pode ser excessivo.
2. Ubiquitous Language e Modelagem Colaborativa
A Ubiquitous Language é o vocabulário comum que todos os membros da equipe usam — desde a conversa no café até o código-fonte. Por exemplo, se especialistas de negócio chamam algo de "Reserva Confirmada", o código deve ter uma classe ou evento com esse mesmo nome, não "OrderApproved".
Técnicas de modelagem colaborativa como Event Storming e Domain Storytelling ajudam a descobrir essa linguagem. Em uma sessão de Event Storming, por exemplo, a equipe inteira coloca post-its em uma parede representando eventos do domínio, comandos e regras de negócio.
O glossário vivo deve ser mantido em um documento acessível (wiki ou README do repositório) e atualizado sempre que a linguagem evolui.
3. Bounded Contexts e Context Mapping
Bounded Contexts são limites explícitos onde um modelo de domínio específico é válido. Eles mapeiam os subdomínios do negócio:
- Core subdomain: a parte mais crítica e diferenciadora do negócio
- Supporting subdomain: necessário, mas não estratégico
- Generic subdomain: soluções comuns que podem ser compradas ou terceirizadas
As estratégias de integração entre contextos incluem:
- Customer/Supplier: um contexto fornece dados que outro consome
- Conformist: um contexto se adapta ao modelo do outro
- Anti-Corruption Layer: uma camada de tradução protege um contexto de modelos externos
Exemplo prático: em um e-commerce, separamos "Vendas" e "Estoque" como bounded contexts distintos. O contexto de Vendas não sabe como o estoque funciona internamente — ele apenas envia um comando "ReservarItem" e recebe uma resposta.
4. Entidades, Value Objects e Agregados
Entidades possuem identidade única e ciclo de vida. Um Pedido (ID #12345) continua sendo o mesmo pedido mesmo que seu status mude.
class Pedido {
id: PedidoId
clienteId: ClienteId
itens: List<ItemPedido>
status: StatusPedido
fun confirmar() {
validarItensDisponiveis()
this.status = StatusPedido.CONFIRMADO
adicionarEvento(PedidoConfirmado(this.id))
}
}
Value Objects são imutáveis e se equivalem por seus atributos. Um Endereço com os mesmos campos é o mesmo endereço, independente de "identidade".
class Endereco {
val rua: String
val numero: String
val cep: String
val cidade: String
}
Agregados são clusters de entidades e value objects tratados como uma unidade. A raiz do agregado (ex: Pedido) garante a consistência transacional de todo o grupo.
class AgregadoPedido {
val raiz: Pedido
// Apenas a raiz expõe métodos públicos
fun aplicarCupom(cupom: Cupom) {
raiz.validarCupom(cupom)
raiz.itens.forEach { item -> item.aplicarDesconto(cupom) }
}
}
5. Domain Events e Comunicação Assíncrona
Domain Events representam algo relevante que ocorreu no domínio. Eles permitem comunicação assíncrona entre bounded contexts sem acoplamento forte.
class PedidoConfirmado(
val pedidoId: String,
val dataConfirmacao: DateTime
) : DomainEvent
class EstoqueInsuficiente(
val pedidoId: String,
val sku: String,
val quantidadeSolicitada: Int,
val quantidadeDisponivel: Int
) : DomainEvent
Exemplo prático: quando um pedido é confirmado, o domínio publica PedidoConfirmado. O contexto de Estoque reage, tentando reservar itens. Se falha, publica EstoqueInsuficiente, que dispara notificação ao cliente e rollback parcial.
// Reação ao evento
fun handle(evento: PedidoConfirmado) {
val resultado = servicoEstoque.reservarItens(evento.pedidoId)
if (!resultado.sucesso) {
publicar(EstoqueInsuficiente(evento.pedidoId, resultado.sku,
resultado.quantidade, resultado.disponivel))
}
}
6. Repositórios, Fábricas e Camada de Aplicação
Repositórios abstraem a persistência, focando em agregados completos. O código de domínio nunca chama o banco diretamente.
interface RepositorioPedido {
fun buscarPorId(id: PedidoId): Pedido?
fun salvar(pedido: Pedido)
fun buscarPorCliente(clienteId: ClienteId): List<Pedido>
}
Fábricas constroem objetos complexos garantindo invariantes. Uma fábrica de Pedido, por exemplo, valida que há ao menos um item antes de criar a instância.
Camada de aplicação orquestra casos de uso sem conter lógica de negócio:
class ServicoAplicacaoPedido(
val repositorio: RepositorioPedido,
val servicoEstoque: ServicoEstoque
) {
fun confirmarPedido(comando: ConfirmarPedido) {
val pedido = repositorio.buscarPorId(comando.pedidoId)
pedido.confirmar()
repositorio.salvar(pedido)
// Dispara eventos
eventBus.publicar(pedido.eventos)
}
}
7. DDD na Prática: Exemplo End-to-End
Cenário: sistema de aluguel de carros com regras de reserva, faturamento e disponibilidade.
Bounded Contexts identificados:
- Reservas (core): gerencia agendamentos e disponibilidade
- Faturamento (supporting): calcula valores e processa pagamentos
- Frota (generic): gerencia manutenção dos veículos
Agregado principal (Reserva):
class Reserva {
val id: ReservaId
val clienteId: ClienteId
val veiculoId: VeiculoId
val periodo: PeriodoLocacao
val valor: ValorLocacao
val status: StatusReserva
fun confirmar() {
require(status == StatusReserva.PENDENTE)
require(periodo.dataInicio.isAfter(LocalDate.now()))
this.status = StatusReserva.CONFIRMADA
adicionarEvento(ReservaConfirmada(this.id, this.veiculoId, this.periodo))
}
fun cancelar(motivo: String) {
this.status = StatusReserva.CANCELADA
adicionarEvento(ReservaCancelada(this.id, motivo))
}
}
Caso de uso: Confirmar Reserva
class ConfirmarReservaHandler(
val repositorio: RepositorioReserva,
val servicoDisponibilidade: ServicoDisponibilidade,
val eventBus: EventBus
) {
fun executar(comando: ConfirmarReserva) {
val reserva = repositorio.buscarPorId(comando.reservaId)
require(servicoDisponibilidade.veiculoDisponivel(reserva.veiculoId, reserva.periodo))
reserva.confirmar()
repositorio.salvar(reserva)
eventBus.publicar(reserva.eventos)
}
}
8. Armadilhas Comuns e Boas Práticas
Armadilhas:
-
Superengenharia: aplicar todos os padrões DDD em um CRUD simples. DDD não é sobre ter Aggregate Roots, Repositories e Domain Events em todo lugar — é sobre modelar o domínio corretamente.
-
Ignorar a linguagem ubíqua: criar classes como
OrderEntity,OrderRepositoryImplenquanto o negócio chama de "Pedido" e "Arquivo de Pedidos". O código deve refletir o vocabulário do negócio. -
Agregados gigantes: colocar tudo dentro de um único agregado por preguiça de modelar, gerando problemas de concorrência e performance.
Boas práticas:
- Comece pelo subdomínio core, onde o DDD agrega mais valor
- Use Event Storming para descobrir o modelo antes de escrever código
- Mantenha agregados pequenos — a regra prática é "mude apenas o que precisa ser consistente"
- Refatore gradualmente: não precisa aplicar DDD do dia para a noite
DDD é uma jornada, não um destino. Comece pequeno, foque no que é essencial para o negócio e evolua com refatoração contínua.
Referências
- Domain-Driven Design Fundamentals - Pluralsight — Curso introdutório que cobre os conceitos fundamentais de DDD com exemplos práticos
- DDD Community - dddcommunity.org — Portal oficial da comunidade DDD com recursos, artigos e referências de livros
- Martin Fowler - BoundedContext — Artigo clássico de Martin Fowler explicando o conceito de Bounded Context
- Event Storming - Alberto Brandolini — Site oficial da técnica de modelagem colaborativa Event Storming, com guias e exemplos
- Microsoft - Domain-Driven Design: A Practical Guide — Guia prático da Microsoft sobre implementação de DDD em microsserviços
- Vaughn Vernon - Implementing Domain-Driven Design — Livro referência sobre implementação prática de DDD, com exemplos em Java e C#
- Eric Evans - Domain-Driven Design: Tackling Complexity in the Heart of Software — O livro original que definiu o DDD, disponível no site do autor