Por que DDD e Kotlin são a dupla ideal
Quando um projeto cresce e os requisitos de negócio começam a parecer um emaranhado confuso, a abordagem CRUD comum deixa de funcionar. Você se pega pensando que metade do código são verificações if-else infinitas, e qualquer mudança na lógica leva à alteração de dezenas de arquivos. Familiar?
É aí que entra o Domain-Driven Design (DDD) — uma abordagem que coloca a lógica de negócios no centro do universo. E o Kotlin, com sua sintaxe concisa, sistema de tipos poderoso e suporte a conceitos funcionais, torna-se a ferramenta ideal para implementar DDD. Neste artigo, vamos explorar como unir essas duas forças para que seu código não seja apenas funcional, mas expressivo e flexível.
Se você programa em Kotlin e quer que sua arquitetura respire, em vez de ranger — está no lugar certo. Hoje não vamos apenas falar de teoria, mas escrever exemplos reais de código.
Domain-Driven Design: um resumo rápido para quem tem pressa
Antes de mergulharmos no código, vamos alinhar os termos. DDD não é um framework nem uma biblioteca. É um conjunto de princípios que ajudam desenvolvedores e negócios a falarem a mesma língua.
Conceitos-chave:
- Ubiquitous Language (Linguagem Ubíqua) — os termos no código devem coincidir com os termos usados por analistas de negócios e clientes.
- Bounded Context (Contexto Delimitado) — um limite claro dentro do qual o modelo faz sentido. Por exemplo, "Cliente" no contexto de vendas e "Cliente" no contexto de suporte são entidades diferentes.
- Entity (Entidade) — um objeto com um identificador único que muda ao longo do tempo.
- Value Object (Objeto de Valor) — um objeto imutável definido por seus atributos, não por um identificador.
- Aggregate (Agregado) — um grupo de objetos relacionados tratados como uma unidade.
- Repository (Repositório) — uma abstração para acesso a dados que oculta os detalhes de armazenamento.
Parece abstrato? Vamos traduzir isso para Kotlin agora.
Value Objects em Kotlin: elegância e segurança
Vamos começar pelo mais agradável — os Value Objects (VO). Em Kotlin, implementá-los é um prazer graças ao data class.
Imagine que estamos trabalhando em um sistema de reservas de hotéis. Temos o conceito de "Período de Estadia". Não é uma entidade — é apenas um conjunto de datas. Um candidato perfeito para VO.
data class StayPeriod( val checkIn: LocalDate, val checkOut: LocalDate) { init { require(checkIn.isBefore(checkOut)) { "A data de check-in deve ser anterior à data de check-out" } }
val totalNights: Int get() = ChronoUnit.DAYS.between(checkIn, checkOut).toInt()
fun overlapsWith(other: StayPeriod): Boolean { return !(this.checkOut.isBefore(other.checkIn) || this.checkIn.isAfter(other.checkOut)) }}Por que isso é legal?
- Imutabilidade — todos os campos são
val. Ninguém alterará acidentalmente a data de check-in. - Validação embutida — o bloco
initgarante que o objeto só seja criado em um estado válido. - Comportamento — o método
overlapsWithencapsula a regra de negócio. Você não procura sobreposição de datas em serviços; pergunta ao próprio objeto.
Em DDD, chamamos isso de modelo rico — o objeto não apenas armazena dados, mas sabe executar operações de negócio.
Entity: quando a identidade importa
Agora vamos para as Entity. Diferente dos VO, uma entidade muda e tem identidade. Em Kotlin, usamos uma classe comum, mas com foco no identificador.
Exemplo — uma reserva de hotel (Booking):
class Booking( val id: BookingId, val roomNumber: Ro