Domain-Driven Design para projetos pequenos
Domain-Driven Design para projetos pequenos: aplicações práticas e recomendações para quem quer adotar os conceitos sem exagero.
O CRUD que virou catedral
Já vi um time pegar um app de cadastro de clientes — literalmente quatro tabelas e um formulário — e transformá-lo num monumento de DDD. Tinha aggregate root, repository, domain event, application service, infrastructure layer, e uma pasta Domain/ com mais arquivos do que o sistema tinha funcionalidades. Para criar um cliente, a requisição passava por sete camadas. Ninguém entendia onde colocar uma validação nova. O código não estava errado por usar DDD; estava errado por usar DDD pesado num problema que não pedia. Cargo cult de arquitetura é caro e silencioso.
Aqui eu separo o que vale a pena roubar do Domain-Driven Design num projeto pequeno e o que dá pra ignorar sem culpa. Se você ainda confunde as peças básicas, vale entender antes as diferenças entre DTO, entity e value object — porque metade do valor prático de DDD em projeto pequeno está em modelar essas três coisas direito.
DDD é dois projetos num nome só
A maior fonte de confusão é tratar DDD como uma coisa única. Não é. São dois conjuntos quase independentes: o estratégico (bounded contexts, ubiquitous language, mapeamento de subdomínios) e o tático (entities, value objects, aggregates, repositories, domain services). Quando alguém diz "vamos usar DDD" e abre uma pasta Infrastructure/, geralmente está falando só do tático — e do tático em excesso.
Num projeto pequeno, o estratégico é onde está quase todo o retorno, e ele é praticamente de graça. O tático tem partes valiosas e partes que são overhead puro. O erro comum é importar a tábua de padrões táticos inteira e ignorar o estratégico, que é justamente o que custa pouco e rende muito.
O que roubar mesmo num projeto pequeno
Quatro ideias do DDD pagam o aluguel em qualquer tamanho de projeto, inclusive num CRUD:
- Ubiquitous language: chame as coisas no código pelo nome que o negócio usa. Se o cliente fala "apólice", não tenha uma classe
Contract. Isso não custa nada e elimina uma classe inteira de bug de tradução mental. - Bounded context: mesmo num monolito, trace fronteiras. "Cobrança" e "Catálogo" podem viver no mesmo deploy, mas não devem compartilhar o mesmo modelo de
Produto— o produto que a cobrança enxerga é diferente do que o catálogo enxerga. - Entity vs value object: distinga o que tem identidade do que é só valor. Um
Pedidotem identidade (dois pedidos com os mesmos itens são pedidos diferentes). UmDinheiroou umEndereçoé value object — comparado por valor, idealmente imutável. - Lógica de domínio fora do controller: a regra de negócio mora no modelo, não no controller nem no service anêmico que só repassa. Isso por si só já melhora 80% dos CRUDs que viram bagunça.
# value object: imutável, comparado por valor
@dataclass(frozen=True)
class Dinheiro:
centavos: int
moeda: str
def somar(self, outro: "Dinheiro") -> "Dinheiro":
if self.moeda != outro.moeda:
raise ValueError("moedas diferentes")
return Dinheiro(self.centavos + outro.centavos, self.moeda)
Note que Dinheiro carrega a regra (não somar moedas diferentes) dentro de si. Isso é DDD tático no seu melhor: o domínio se protege, e nenhum controller precisa lembrar de validar.
O que pular sem dó
E aqui está a parte que ninguém te diz: a maior parte do catálogo tático de DDD é overhead num projeto pequeno.
- Event sourcing: guardar todo estado como sequência de eventos é poderoso e caríssimo de manter. Num CRUD, você quer uma tabela
clientes, não um event store com projeções. Só considere se auditoria total de mudanças for requisito de negócio real. - CQRS: separar modelo de leitura e escrita resolve um problema de escala que você provavelmente não tem. Um único modelo com queries normais serve a 99% dos projetos pequenos.
- Aggregates em tudo: aggregate root faz sentido quando há invariantes complexas entre várias entidades que precisam mudar juntas em transação. Forçar um aggregate para cada tabela é cerimônia. Muito CRUD tem zero invariante interna — é só dado.
- Repository abstrato por cima de cada tabela: se seu repository só faz
saveefind_by_idrepassando pro ORM, ele é uma camada de indireção sem ganho. O ORM já é seu repository.
A pergunta para cada padrão tático é: que invariante de domínio ou que complexidade real ele protege? Se a resposta for "nenhuma, mas é DDD", corte.
A armadilha do cargo cult
O modo como DDD dá errado em projeto pequeno quase sempre tem a mesma assinatura: alguém leu o livro azul, gostou dos diagramas, e replicou as camadas sem o problema que as justifica. O sintoma é um domínio rico de pastas e pobre de regra — você abre Domain/ e encontra entidades anêmicas, com getters e setters, e toda a lógica de negócio espalhada por application services que viram um novo tipo de controller, só que mais longe.
O teste honesto: se você remover metade das camadas e o código ficar mais fácil de entender sem perder nenhuma regra de negócio, aquelas camadas eram cargo cult. DDD não é sobre ter as pastas certas. É sobre o modelo refletir o domínio e a lógica morar onde a regra vive.
Adoção pragmática
A receita que funciona em projeto pequeno é incremental e começa pelo barato:
- Adote a ubiquitous language hoje — é só renomear coisa.
- Trace os bounded contexts no papel, mesmo que tudo viva num monolito.
- Modele entities e value objects com a lógica dentro deles, tirando regra do controller.
- Adicione padrão tático pesado só quando uma complexidade concreta aparecer — uma invariante real entre entidades, uma necessidade real de auditoria, um gargalo real de leitura.
Comece simples e deixe a complexidade do domínio puxar os padrões, não o contrário. Um CRUD que cresce vai te avisar quando precisar de um aggregate de verdade. Antes disso, aggregate é só uma palavra cara para uma tabela.
Perguntas frequentes
DDD vale a pena num projeto pequeno?
A parte estratégica e a modelagem de entities/value objects, sim — custam pouco e melhoram muito a clareza. O catálogo tático pesado (event sourcing, CQRS, aggregates em tudo), não — é overhead. DDD num projeto pequeno é roubar as ideias baratas e ignorar as caras.
Preciso das camadas Domain/Application/Infrastructure num CRUD?
Quase nunca. Essa separação resolve acoplamento em sistemas complexos. Num CRUD pequeno, ela costuma adicionar indireção sem proteger nenhuma regra. Mantenha a lógica de domínio nos modelos e fora dos controllers; isso já entrega o essencial sem três camadas de cerimônia.
Qual a diferença entre o DDD estratégico e o tático?
O estratégico é sobre fronteiras e linguagem: bounded contexts, ubiquitous language, mapeamento de subdomínios — o desenho de alto nível. O tático é sobre os blocos de construção dentro de um contexto: entities, value objects, aggregates, repositories. Em projeto pequeno o estratégico rende mais e custa menos.
Como sei se estou fazendo cargo cult de DDD?
Se você adicionou padrões porque "é assim que DDD se faz", não porque um problema concreto exigiu, é cargo cult. O teste: remova a camada e veja se alguma regra de negócio ou invariante se perde. Se nada se perde e o código fica mais claro, a camada era decoração.
O que levar para o próximo projeto
DDD num projeto pequeno não é adotar o livro inteiro — é ter discernimento sobre quais ideias pagam o aluguel no seu tamanho de problema. Roube a ubiquitous language, os bounded contexts, a distinção entity/value object e a regra de manter lógica fora do controller; essas coisas são quase de graça e melhoram qualquer CRUD. Pule event sourcing, CQRS e aggregates compulsórios até que uma complexidade real os justifique. E desconfie de toda camada que você não consegue justificar com uma invariante concreta — provavelmente ela está ali por imitação, não por necessidade. Deixe o domínio puxar a complexidade, nunca o contrário.
- 01 Licenças MIT, Apache e GPL: diferenças práticas para devs MIT, Apache 2.0 e GPL não são a mesma coisa. Entenda permissiva vs copyleft, proteção de patentes e compatibilidade antes de colocar código em produção.
- 02 Como desfazer alterações no Git com segurança: reset, revert, restore e reflog Qual comando usar para desfazer alterações no Git em cada situação — reset, revert, restore, stash e reflog — sem perder trabalho.