Se você já trabalhou em um projeto que começou simples, mas rapidamente virou um emaranhado de regras de negócio espalhadas pelo código, nomes confusos e mudanças difíceis de implementar, então este conteúdo é para você. DDD, ou Domain-Driven Design, é uma abordagem de desenvolvimento de software criada exatamente para resolver esse tipo de problema, aproximando o código da realidade do negócio e facilitando a evolução do sistema ao longo do tempo.
De forma simples, DDD é uma maneira de pensar, modelar e organizar o software com foco no domínio, ou seja, no problema real que o sistema precisa resolver. Em vez de começar escolhendo frameworks, bancos de dados ou detalhes técnicos, o DDD propõe que o ponto de partida seja o entendimento profundo do negócio, suas regras, seus termos e seus processos. O código passa a refletir essa linguagem e essa lógica de forma clara e intencional.
Isso não significa que DDD seja algo exclusivo para sistemas gigantes ou times enormes. Pelo contrário: mesmo em projetos pequenos ou médios, aplicar conceitos de DDD ajuda a manter o código mais legível, menos acoplado e muito mais fácil de manter. Frameworks modernos como o NestJS se encaixam muito bem com essa abordagem, pois incentivam organização, separação de responsabilidades e uma arquitetura mais limpa.
Neste artigo, vamos conversar sobre DDD de forma tranquila e prática, sem termos excessivamente acadêmicos. A ideia é que você entenda o porquê do DDD, quando faz sentido usá-lo e como ele pode melhorar a qualidade do seu código no dia a dia. Ao longo dos próximos tópicos, traremos exemplos práticos, inclusive com NestJS, para deixar tudo mais concreto e aplicável à sua realidade como desenvolvedor.
O que significa “Domínio” no DDD?
Antes de falar de padrões, pastas, entidades ou qualquer detalhe técnico, a primeira coisa que você precisa entender em DDD é o conceito de domínio. Esse termo aparece o tempo todo e, apesar de parecer complexo à primeira vista, ele é bem simples na prática.
Domínio nada mais é do que o problema que o seu software existe para resolver. É o universo de regras, processos, termos e comportamentos do negócio. Se você está construindo um sistema para uma clínica médica, o domínio envolve coisas como pacientes, consultas, médicos, agendamentos, convênios, regras de cancelamento e por aí vai. Se for um e-commerce, o domínio gira em torno de produtos, pedidos, pagamentos, entregas, descontos e estoque.
Um erro muito comum, principalmente no início da carreira, é confundir domínio com tecnologia. Framework, banco de dados, filas, APIs… tudo isso é importante, mas não é o domínio. O domínio existe independentemente da linguagem que você usa ou do framework escolhido. Ele continuaria o mesmo se você estivesse programando em Java, Node.js, Python ou até usando planilhas.
No DDD, o domínio é tratado como a parte mais importante do sistema. Ele não deve ficar escondido dentro de controllers, services genéricos ou arquivos utilitários. Pelo contrário: ele deve ser explícito, bem modelado e fácil de entender, até mesmo para alguém que não é desenvolvedor.
É aqui que entra um dos grandes diferenciais do DDD: a proximidade com o negócio. Em vez de o código ser apenas uma tradução técnica de requisitos, ele passa a representar fielmente como o negócio funciona. Quando você lê o código, você entende o problema que ele resolve.
Linguagem Ubíqua: quando o código fala a língua do negócio
Agora que já entendemos o que é domínio, vamos falar de um dos conceitos mais importantes do DDD: a Linguagem Ubíqua. Esse nome pode soar estranho no começo, mas a ideia por trás dele é extremamente prática e poderosa.
Linguagem ubíqua significa que todo mundo envolvido no projeto fala a mesma língua: desenvolvedores, pessoas de negócio, product owners, analistas e stakeholders. Os termos usados em reuniões, documentos, histórias de usuário e, principalmente, no código devem ser os mesmos. Nada de traduzir conceitos de negócio para nomes técnicos genéricos que só fazem sentido para quem programou.
Por exemplo, imagine um sistema financeiro onde, nas reuniões, o pessoal do negócio fala em “fatura”, “parcelamento” e “limite de crédito”, mas no código você encontra classes chamadas PaymentData, ProcessHandler ou GenericService. Isso cria um abismo entre o que o sistema faz e o que o código parece fazer. No DDD, isso é um sinal claro de problema.
Com linguagem ubíqua, se o negócio fala “Pedido”, o código também fala “Pedido”. Se existe uma regra chamada “Pedido só pode ser cancelado antes do envio”, essa regra deve estar explícita em um método como pedido.cancelar(), e não escondida em um if perdido dentro de um controller.
Esse conceito se encaixa muito bem com o NestJS. Em vez de criar serviços genéricos como OrderService cheio de métodos soltos, você pode criar entidades e classes que representam exatamente o domínio: Pedido, Cliente, ItemDoPedido, cada uma com comportamentos claros e nomes que fazem sentido para quem entende do negócio.
A linguagem ubíqua reduz ruído, evita mal-entendidos e facilita muito a manutenção do sistema. Quando alguém novo entra no projeto, ela não precisa aprender “o jeito do código”, porque o código já reflete a realidade do negócio.
Especialistas de domínio: quem realmente entende o problema
Para que DDD funcione de verdade, não basta o desenvolvedor “imaginar” como o negócio funciona. É aqui que entram os especialistas de domínio, também conhecidos como domain experts. Essas pessoas são aquelas que realmente entendem o problema que o sistema precisa resolver no dia a dia.
O especialista de domínio não é, necessariamente, alguém técnico. Na maioria dos casos, é alguém do negócio: um gerente, um analista, um atendente experiente, um contador, um médico, um corretor, depende totalmente do contexto do sistema. É essa pessoa que conhece as regras reais, as exceções, os casos estranhos que quase nunca aparecem, mas que quebram o sistema quando surgem.
No DDD, o desenvolvedor trabalha lado a lado com o especialista de domínio. Não é um processo de “receber requisitos prontos”, implementar e pronto. É uma conversa constante. Perguntar “por que isso funciona assim?”, “o que acontece se isso der errado?”, “essa regra vale sempre ou só em alguns casos?” faz parte do trabalho.
Essa colaboração influencia diretamente o código. Conforme as conversas acontecem, a linguagem ubíqua vai se refinando. Termos são ajustados, conceitos são separados, responsabilidades ficam mais claras. Muitas vezes, uma regra que parecia simples se revela complexa, e o modelo de domínio precisa evoluir junto.
Em projetos usando NestJS, isso se reflete na forma como você organiza suas pastas e classes. Em vez de um único service concentrando toda a lógica, você começa a criar objetos que representam decisões do negócio. O código deixa de ser apenas uma sequência de passos técnicos e passa a expressar intenções.
Um ponto importante: DDD não elimina documentação nem comunicação. Pelo contrário, ele depende delas. Reuniões de refinamento, conversas frequentes e validações constantes são essenciais para garantir que o código continue alinhado com o que o negócio realmente precisa.
Entidades e Objetos de Valor: entendendo a diferença
Quando começamos a modelar o domínio no DDD, dois conceitos aparecem o tempo todo: Entidades e Objetos de Valor. Entender bem a diferença entre eles é fundamental para criar um modelo de domínio mais claro, expressivo e fácil de manter.
Vamos começar pelas Entidades. Uma entidade é algo que possui identidade própria ao longo do tempo. Mesmo que seus atributos mudem, ela continua sendo a mesma coisa. Pense em um Cliente, um Pedido ou um Usuário. Um cliente pode mudar de endereço, de telefone ou de email, mas ele continua sendo o mesmo cliente. No código, isso geralmente é representado por um identificador único, como um id.
No DDD, entidades não são apenas estruturas de dados. Elas possuem comportamento. As regras do negócio vivem nelas. Por exemplo, em vez de um service decidir se um pedido pode ou não ser cancelado, essa regra pode estar dentro da própria entidade Pedido, em um método como cancelar(). Isso deixa o código mais coeso e muito mais fácil de entender.
Já os Objetos de Valor funcionam de forma diferente. Eles não possuem identidade. O que importa neles são os valores que carregam. Se dois objetos de valor têm os mesmos atributos, eles são considerados iguais. Um bom exemplo é um Endereço, um CPF, um Dinheiro ou um Período. Se dois endereços têm os mesmos dados, não faz sentido tratá-los como coisas diferentes.
Objetos de valor costumam ser imutáveis. Em vez de alterar um endereço, você cria um novo. Isso reduz efeitos colaterais e torna o sistema mais previsível. Além disso, eles são ótimos lugares para centralizar validações. Um CPF inválido simplesmente não deveria existir no sistema.
Em projetos NestJS, essa separação ajuda muito a evitar models anêmicos. Em vez de usar apenas DTOs e services genéricos, você cria entidades e objetos de valor ricos em comportamento, mantendo a lógica do negócio onde ela realmente pertence.
Agregados e Aggregate Root: controlando a complexidade do domínio
À medida que o domínio cresce, as entidades começam a se relacionar entre si, e o risco de criar dependências confusas aumenta bastante. É para resolver esse problema que o DDD apresenta o conceito de Agregados e Aggregate Root.
Um Agregado é um conjunto de entidades e objetos de valor que formam uma unidade de consistência. Isso significa que, dentro desse conjunto, as regras de negócio devem ser sempre mantidas válidas. Já o Aggregate Root é a entidade principal desse agregado, a única que pode ser acessada diretamente de fora.
Vamos a um exemplo simples: um Pedido. Ele pode conter vários ItensDoPedido, um EndereçoDeEntrega e talvez informações de pagamento. Todos esses elementos fazem parte do mesmo agregado. O Pedido é o Aggregate Root. Você não deveria, por exemplo, alterar um ItemDoPedido diretamente sem passar pelo Pedido. Toda modificação deve acontecer através do root.
Isso traz várias vantagens. A principal é o controle da consistência. Se existe uma regra dizendo que um pedido cancelado não pode ter itens alterados, essa regra fica centralizada no Pedido. Não importa de onde a ação venha — controller, fila, evento — o agregado garante que o estado do sistema continue válido.
No NestJS, isso se traduz em um design mais claro. Repositórios geralmente trabalham com o aggregate root, não com entidades internas. Em vez de ter um ItemPedidoRepository, você teria um PedidoRepository, responsável por salvar e recuperar o agregado como um todo.
Outro ponto importante é que agregados ajudam a definir limites claros de transações. Tudo que acontece dentro de um agregado deve ser consistente no final da operação. Isso evita transações gigantes e dependências desnecessárias entre partes diferentes do sistema.
Um erro comum é criar agregados grandes demais. DDD não significa colocar tudo dentro de um único objeto. Pelo contrário: bons agregados são pequenos, focados e bem definidos.
Repositórios: conectando o domínio ao mundo externo
Depois de modelar entidades, objetos de valor e agregados, surge uma pergunta natural: como esses objetos são salvos e recuperados do banco de dados? No DDD, essa responsabilidade é do Repositório.
Um repositório funciona como uma coleção de agregados em memória, escondendo completamente os detalhes de persistência. O domínio não deve saber se os dados estão sendo salvos em PostgreSQL, MongoDB, Redis ou qualquer outro banco. Ele apenas pede ou salva objetos através de uma interface bem definida.
Por exemplo, em vez de o domínio fazer consultas SQL ou usar diretamente um ORM, ele depende de algo como PedidoRepository. Esse repositório oferece métodos que fazem sentido para o negócio, como buscarPorId, salvar ou buscarPedidosAbertosPorCliente. Nada de métodos genéricos demais ou expor detalhes técnicos.
No NestJS, isso se encaixa muito bem com o conceito de injeção de dependência. Você pode definir uma interface de repositório dentro da camada de domínio e ter uma implementação concreta na camada de infraestrutura, usando TypeORM, Prisma ou qualquer outra ferramenta. O domínio continua limpo e independente.
Essa separação traz flexibilidade. Se amanhã você precisar trocar o banco de dados ou mudar a forma de persistência, o impacto no domínio é mínimo. Além disso, os testes ficam muito mais simples, já que você pode usar repositórios em memória ou mocks para validar as regras de negócio sem depender de infraestrutura.
Um ponto importante: repositórios trabalham com Aggregate Roots, não com qualquer entidade do sistema. Isso reforça os limites definidos pelos agregados e evita acessos indevidos a partes internas do domínio.
Serviços de Domínio: quando a regra não pertence a uma entidade
Até aqui, vimos que no DDD a maior parte das regras de negócio deve viver dentro das entidades e dos agregados. Mas, na prática, nem toda regra se encaixa bem em um único objeto. É nesse cenário que entram os Serviços de Domínio.
Um serviço de domínio representa uma operação do negócio que envolve mais de uma entidade ou que não faz sentido pertencer a apenas uma delas. A principal característica dele é que ele não possui estado próprio. Ele existe apenas para executar uma regra ou processo específico.
Imagine uma regra como: “transferir saldo entre duas contas”. Essa lógica envolve duas entidades Conta. Colocar essa regra em apenas uma delas pode gerar confusão. Nesse caso, um ServicoDeTransferencia faz muito mais sentido, coordenando a operação de forma clara e explícita.
No DDD, serviços de domínio devem usar linguagem do negócio, assim como entidades e agregados. Nada de nomes técnicos como ProcessService ou HelperService. O nome do serviço precisa deixar claro o que ele representa dentro do domínio.
No contexto do NestJS, é muito comum confundir serviços de domínio com os @Injectable() services tradicionais do framework. Eles até podem ser implementados como providers, mas conceitualmente são coisas diferentes. Um service de domínio não deveria conhecer controllers, bancos de dados ou detalhes de infraestrutura. Ele trabalha apenas com objetos do domínio e interfaces de repositórios.
Um erro frequente é criar um “service” que vira um Deus do sistema, com centenas de métodos e regras espalhadas. Isso vai totalmente contra o DDD. Se um service começa a crescer demais, é um sinal claro de que parte dessa lógica deveria estar em entidades, agregados ou até em um novo conceito do domínio.
Usados com cuidado, serviços de domínio ajudam a manter o modelo limpo, expressivo e alinhado com o negócio.
Bounded Context: organizando domínios complexos
À medida que um sistema cresce, é comum perceber que a mesma palavra começa a ter significados diferentes dependendo da área do negócio. É exatamente para lidar com essa complexidade que o DDD introduz o conceito de Bounded Context, ou contexto delimitado.
Um bounded context define um limite claro onde um determinado modelo de domínio é válido. Dentro desse limite, os termos, regras e significados são consistentes. Fora dele, esses mesmos termos podem representar coisas completamente diferentes.
Um exemplo clássico é a palavra “Pedido”. Em um e-commerce, o pedido no contexto de vendas pode representar a intenção de compra do cliente. Já no contexto de logística, pedido pode significar algo pronto para separação e envio. As regras, atributos e comportamentos são diferentes, mesmo usando o mesmo nome. No DDD, esses seriam dois bounded contexts distintos.
O grande erro que o DDD evita é tentar criar um modelo único para tudo. Isso normalmente leva a entidades gigantes, cheias de if e regras condicionais que tentam atender todos os cenários possíveis. Com bounded contexts, cada parte do sistema tem seu próprio modelo, focado em resolver um problema específico.
No NestJS, isso se reflete muito bem na organização do projeto. Em vez de separar por camadas técnicas (controllers, services, repositories globais), você pode separar por contexto: vendas, pagamentos, logistica, por exemplo. Cada contexto tem suas próprias entidades, serviços e repositórios, sem dependências diretas entre si.
A comunicação entre bounded contexts acontece através de contratos bem definidos, como eventos, mensagens ou APIs. Isso reduz acoplamento e permite que cada parte evolua no seu próprio ritmo.
Bounded context é um dos conceitos que mais traz clareza em sistemas grandes, mas ele também pode ser aplicado de forma simples em projetos menores.
Arquitetura em camadas e DDD no NestJS
Quando falamos em DDD na prática, uma das maiores dúvidas é: como organizar o projeto? Onde ficam as entidades? Onde entra o NestJS? Como evitar misturar regra de negócio com detalhes técnicos? A resposta está em uma arquitetura em camadas bem definida, respeitando os princípios do DDD.
De forma geral, quando usamos DDD com NestJS, o sistema costuma ser dividido em pelo menos quatro camadas principais:
- Domínio: é o coração da aplicação. Aqui ficam entidades, objetos de valor, agregados, serviços de domínio e interfaces de repositórios. Essa camada não conhece NestJS, banco de dados ou frameworks externos.
- Aplicação: coordena os casos de uso. Ela orquestra o domínio, chamando entidades, serviços de domínio e repositórios para executar uma ação específica, como “criar pedido” ou “cancelar assinatura”.
- Infraestrutura: contém as implementações técnicas, como acesso a banco de dados, integrações externas, filas, cache e APIs.
- Interface (ou apresentação): onde entram controllers, handlers de eventos, REST, GraphQL, etc.
O NestJS normalmente vive nas camadas de aplicação, infraestrutura e interface, mas o domínio deve permanecer o mais puro possível. Isso significa que uma entidade não deve depender de decorators do NestJS nem de classes específicas do framework.
Na prática, a estrutura de pastas pode seguir algo assim:
src/
pedidos/
domain/
entities/
value-objects/
repositories/
services/
application/
use-cases/
infrastructure/
repositories/
persistence/
presentation/
controllers/
Essa organização ajuda muito na leitura e na manutenção do código. Quando alguém entra no projeto, fica claro onde estão as regras do negócio e onde estão os detalhes técnicos.
Vale lembrar que DDD não é uma receita fixa. O objetivo não é criar uma estrutura perfeita, mas sim manter clareza, separação de responsabilidades e alinhamento com o negócio.
Conclusão
Domain-Driven Design não é um framework, não é uma arquitetura engessada e muito menos uma moda passageira. DDD é, acima de tudo, uma forma de pensar software a partir do problema real, colocando o domínio e o negócio no centro das decisões técnicas. Quando bem aplicado, ele ajuda a criar sistemas mais legíveis, organizados e preparados para mudanças, algo que todo projeto inevitavelmente enfrenta.
Ao longo deste artigo, vimos que DDD começa com entendimento: entender o domínio, conversar com especialistas, construir uma linguagem comum e transformar isso em código que faz sentido. Conceitos como entidades, objetos de valor, agregados, repositórios e bounded contexts existem para reduzir complexidade, não para aumentá-la. Se eles estiverem tornando o projeto mais difícil de entender, algo está errado na aplicação, não no DDD em si.
Também é importante reforçar que DDD não é obrigatório para todos os projetos. Em sistemas muito simples, ele pode ser um exagero. Mas à medida que o negócio cresce, as regras se multiplicam e o time aumenta, os benefícios ficam cada vez mais claros. Frameworks como o NestJS facilitam bastante essa abordagem, justamente por incentivarem organização e separação de responsabilidades.
Se você nunca aplicou DDD, não tente implementar tudo de uma vez. Comece pequeno. Use melhor os nomes, traga regras para perto das entidades, converse mais com o negócio. DDD é uma jornada, não um checklist.
Agora eu quero ouvir você. Você já tentou aplicar DDD em algum projeto? Teve dificuldades ou bons resultados? Deixa seu comentário aqui embaixo e vamos continuar essa conversa.