DEV Community

Yuri Peixinho
Yuri Peixinho

Posted on

Domain-Driven Design (DDD)

Introdução

O DDD é uma abordagem ao design de software que se baseia no conceito de domínio; é um filosofia voltada ao domínio do negócio. Desse modo, a principal ideia é de que o mais importante em um software não é seu código, nem arquitetura ou mesmo a tecnologia sobre a qual foi desenvolvido, mas sim o problema que aquele sistema se propõe resolver, ou em outras palavras, a regra de negócio.

Sua grande vantagem é que não requer o uso de nenhuma arquitetura específica. Como já dito anteriormente, o DDD não é uma arquitetura, ele é um conceito que pode ser utilizado em diversas aquitetura como por exemplo, na implementação de arquiteturas como: Clean Architecture, Onion Architecture , Hexagonal Architecture, CQRS, REST, etc.

No livro “Domain-Driven Design Tackling Complexity in the Hearth of Software”, Eric Evans definiu um conjunto de conceitos básicos para apresentar o Domain Drive Design. Ele catalogou uma série de boas práticas que compõe a abordagem que conhecemos como DDD.

DDD preza que os desenvolvedores façam parte do processo de entender o negócio e todos os seus modelos nos mais diversos ângulos ao invés de simplesmente entrevistar o especialista.

Quando usar o DDD?

  • Quando do domínio é complexo e as regras mudam com frequência
  • Há muitos termos e comportamentos específicos do negócio
  • Você precisa de manutenção e evolução a longo prazo

Quando evitar o DDD?

  • É apenas um projeto de CRUD simples
  • O domínio é estável e pouco mutável

Design Estratégicos (Strategic Design) vs. Design Táticos (Tactical Design)

Antes de nos aprofundarmos sobre os principais conceitos do DDD temos que entender que o Domain-Driven Design é dividido em dois grandes níveis de atuação: o estratégico e o tático.

Em tese, ambos se complementam:

  • O estratégico guia a estrutura e comunicação entre partes do sistema.
  • O tático guia a implementação e comportamento dentro dessas partes.

Design Estratégico

Trata do nível macro do sistema, é a visão de negócio e a sua estrutura geral. O objetivo é entender o domínio, dividi-lo em partes coerentes e definir limites claros entre essas partes.

É aqui que ocorre o planejamento conceitual do domínio antes de qualquer linha de código.

Principais responsabilidades:

  • Entender profundamente o negócio e seu subdomínios
  • Delimitar o Bounded Contexts
  • Definir os contextos e como se relacionam (Context Map)
  • Estabelecer a Línguagem Ubíqua entre os especialistas e os desenvolvedores
  • Organizar fronteiras e integrações entre as partes do sistema

Design Tático

O design tático atua no nível micro, dentro de cada Bounded Context

Ele define como o modelo de domínio será implementado no código, aqui o foco sai da organização macro e entra nos padrões de modelagem de software que traduzem o domínio em estruturas reais.

Principais responsabilidades

  • Criar um modelo de domínio rico e expressivo
  • Definir Entidades e Objetos de Valor que representam o negócio
  • Estruturar Agregados para manter consistência e regras
  • Utilizar repositórios, serviços de domínio, eventos de domínios
  • Garantir que a lógica do negócio esteja concentrada no núcleo do domínio, e não dispersa pela aplicação

Principais conceitos

Existem três pilares fundamentais quando vamos falar sobre DDD, são eles: Linguagem Ubíqua, Bounded Contexts e Context Maps.

Ubiquitous Language (Linguagem Onipresente)

Um dos pilares do DDD é a linguagem Onipresente. A ideia é que todos falem o mesmo idioma, sem “traduções mentais” entre o que o negócio diz e o negócio faz. Ela é uma linguagem comum e compartilhada entre os desenvolvedores, especialista do domínio, analistas, testadores, PO, etc. Essa linguagem é utilizada em todas as conversas, na modalagem no código e até na documentação.

Exemplo

Imagine que você está desenvolvendo um sistema bancário. O especialista de domínio fala: “Um cliente pode fazer uma Transferência entre contas e isso gera um lançamento no extrato”.

Em muitos oprojetos, o código acaba refletindo em outra linguagem como:

ExecuteTransaction(Account a, Account b, decimal value)
Enter fullscreen mode Exit fullscreen mode

Mas no DDD, aplicando a Linguagem Onipresente, o código deve refletir o domínio real:

contaOrigem.TransferirPara(contaDestino, valor);
Enter fullscreen mode Exit fullscreen mode

E as classes deveriam se chamar Conta, Transferencia, Lancamento, etc. Exatamente como o domínio fala.

Negócio ───> Linguagem Ubíqua ───> Modelo ───> Código
          (ponte entre especialistas e devs)
Enter fullscreen mode Exit fullscreen mode

Por que é importante?

Ela reduz a ambiguidades e garante que o modelo e negócio evoluam juntos. Existe um conceito chamado Abismo do modelo (model gap), que surge quando o desenvolvedor e o especialista usam linguagens diferentes.

Por exemplo:

  • Especialista fala “cancelar um pedido”
  • No código tem um método DeleteOrder()

O problema é que “cancelar” ≠ “deletar” no domínio. Cancelar pode ter regras de negócio (reembolso, estoque, histórico, etc.), e deletar simplesmente remove o registro.

Exemplo prático

Um código de E-commerce.

Os especialistas dizem:

“Um cliente faz um pedido, que contém itens.”
“Esse pedido pode ser confirmado, cancelado ou entregue

public class Pedido
{
    private readonly List<ItemPedido> _itens = new();

    public void AdicionarItem(Produto produto, int quantidade)
        => _itens.Add(new ItemPedido(produto, quantidade));

    public void Confirmar() { /* ... */ }
    public void Cancelar() { /* ... */ }
}:
Enter fullscreen mode Exit fullscreen mode
  • Pedido, ItemPedido, Produto, Confirmar, Cancelar — são as mesmas palavras do negócio.
  • Isso facilita conversas entre devs e especialistas.
  • O modelo é o domínio, não uma abstração técnica.

Bounded Context (Conceito Delimitado)

Define um limite explícito dentro do qual um modelo de domínio é válido e consistente. Em sistemas grandes, o domínio tende a se tornar complexo e ambíguo. Por exemplo, a palavra “Cliente” pode significar coisas diferentes para diferentes partes do negócio.

  • No contexto de Vendas, Cliente é quem faz o pedido e gera receita
  • No contexto de suporte, Cliente é quem abre chamados e tem históricos de atendimentos
  • No contexto Financeiro, Cliente é uma entidade com crédito, saldo, faturas, etc.

Desse modo, cada um desses significados forma seu próprio modelo de domínio e cada modelo vive em seu próprio Bounded Context.

+----------------------+
| Contexto de Vendas   |
|  - Pedido            |
|  - Produto           |
|  - Cliente           |
+----------------------+

+----------------------+
| Contexto Financeiro  |
|  - Fatura            |
|  - Pagamento         |
|  - Cliente           |
+----------------------+

+----------------------+
| Contexto de Suporte  |
|  - Chamado           |
|  - Cliente           |
+----------------------+
Enter fullscreen mode Exit fullscreen mode

Organizando desse modo, todos os termos, regras e comportamentos de domínio têm um significado único e coerente, a famosa Ubiquitous Language (linguagem ubíqua) é usada de forma consistente apenas dentro desse contexto. Fora dele, as palavras ou conceitos pode significar outra coisa.

Strategic Design (Modelagem Estratégica)

Essa etapa é o conjunto de princípios que ajudam a organizar e dividir o domínio de um sistema complexo em partes coerentes, delimitadas e bem integradas. É aqui onde você pensa com estratégia antes de mergulhar nos detalhes de códigos e entidades.

É nessa etapa que vamos definir os contextos que será utilizado no sistema.

Context Map

No momento que conseguimos definir nossos contextos, a gente entra em um segundo ponto, chamado de Context Map.

Ele é um mapa visual ou conceitual que mostra como os diferentes Boundeds Contexts de um sistema se relacionam entre si

O seu objetivo é tornar explícitas as dependências, integrações e relações de poder entre equipes e contextos de domínio.

É como um mapa político de um país, cada contexto é um “território” com suas próprias leis (modelos de domínio) e fronteiras bem definidas

Tipos de relacionamentos entre contextos

DDD define padrões de relacionamentos (inspirados em padrões de relações entre times)

Tipo Descrição
Partnership (Parceria) Dois contextos trabalham juntos de forma colaborativa e coordenada.
Shared Kernel (Núcleo Compartilhado) Compartilham uma pequena parte do modelo de domínio (um módulo comum).
Customer/Supplier (Cliente/Fornecedor) Um contexto depende do outro, que fornece dados ou serviços.
Conformist (Conformista) O contexto consumidor aceita o modelo do fornecedor sem questionar (baixa autonomia).
Anti-Corruption Layer (Camada Anti-Corrupção) Um contexto protege seu modelo interno criando uma camada de tradução entre ele e o fornecedor.
Open Host Service (OHS) Um contexto expõe um contrato formal (API, protocolo) para integração.
Published Language (Linguagem Publicada) Um formato de comunicação padronizado entre contextos (por exemplo, eventos, schemas).
Separate Ways (Caminhos Separados) Contextos independentes que não precisam se integrar.

Domain (Domínio)

Domínio é o coração do negócio que a equipe está trabalhando. É baseado em um conjunto de ideias, conhecimento e processo de negócio. Podemos dizer que o domínio é o “mundo real” que o software está tentando modelar.

Por exemplo:

  • Se é um sistema de vendas, o domínio trata de pedidos, clientes, pagamentos, produtos, descontos, etc.
  • Se é de saúde, o domínio lida com pacientes, consultas, diagnósticos, prescrições, e assim por diante

No DDD o domínio é o centro de tudo. Ele define que todas as decisões técnicas tomadas para refletir e respeitar o modelo do domínio, e não o contrário. A lógica do negócio, então (regras e comportamentos essenciais) percentem ao domínio, e não devem depender de infraestrutura (como banco de dados, frameworks, etc.).

Domain Model (Modelo de domínio)

Esse segundo conceito é a representação do domínio (domain) dentro do código. Ela é uma abstração da realidade do domínio, criada em conjunto por desenvolvedores e especialistas do negócio, usando a linguagem ubíquia (Ubiquitous Language).

“O model é a tradução, em código, da maneira como o domínio funciona no mundo real”

Domain Model Pattern

Agora que você já entendeu sobre a línguagem ubíqua, o contexto delimitado e o seu mapa de conceito delimitado, vamos começar a falar sobre os pontos de implementação. Geralmente os desenvolvedores relevam todos os pontos descritos anteriormente e focam direto na implementação, que não é uma boa prática. No Domain Model Pattern você vai perceber que o objetivo é evitar o modelo anêmico e o foco é que o domain da aplicação seja um modelo rico em comportamento e não apenas como um conjunto de tabelas ou estruturas de dados.

Podemos dizer que o Domain Model Patern são um conjunto de padrões de projeto (design pattern) que são utilizados para compor os layers no DDD.

  • São padrões de desenvolvimento e estruturação de aplicações cujo domínio é o principal foco (Domain Events)
  • Objetos de valor (Value Objects)
  • Focado em Entidades de Agregadores (Aggregates)
  • Repositórios (Repositories)
  • Serviços (Domain Services)

1. Objetos de Valor (Value Objects)

Geralmente trabalhamos com dados primitivos, por exemplo, CPF como string. Chamamos isso de ”Primitive Obsession”, uma “obsessão por tipos primitivos”. Isso ocorre quando utilizamos tipos genéricos (string, int, decimal…) para representar algo que tem semântica específica no domínio, resultando em códigos:

  • Mais frágil (qualquer string é aceita como CPF)
  • Mais repetitivo (validação e formatação se espalham)
  • Menos expressivo (não fica claro o que aquele valor representa)

Por exemplo, no código abaixo temos um tipo string para representar um CPF, inclusive "banana".

public class Pessoa {
    public string Cpf { get; set; }

    public void Cadastrar(string cpf)
    {
        if (string.IsNullOrEmpty(cpf))
            throw new ArgumentException("CPF obrigatório.");

        if (!Regex.IsMatch(cpf, @"^\d{11}$"))
            throw new ArgumentException("CPF inválido.");

        // e assim vai...
        Cpf = cpf;
    }
}
Enter fullscreen mode Exit fullscreen mode

Além disso, quando utilizamos CPF como uma string a entidade (ou até mesmo a camada de aplicação precisa se preocupar com

  • Verificar se o campo não está vazio
  • Se tem 11 dígitos
  • Se não tem letras
  • Se o dígito verificado é válido
  • Se deve ser formatado com máscara ou não

Desse modo, a entidade está validando algo que não é responsabilidade dela, além de que essa lógica vai se repetir em outros lugares, como Cliente, Fornecedor, etc. Se essa regra mudar, você precisa caçar por todos os pontos do código, deixando mais difícil testar e manter.

Value Object

É nesse cenário que entra um conceito importantissimo quando falamos de DDDD. Value Object (VO). Ele é uma unidade conceitual do domínio que representa um valor imutável e sem identidade própria. Ou seja, ele é definido pelo seu conteúdo e não seu identificador (ID).

  • Se dois CPFs têm o mesmo número, então eles são o mesmo CPF
  • Se dois endereços têm os mesmos campos (rua, número, cidade), são os mesmos endereços

o foco está no valor em si, e não em quem ele “é”.

Regras típicas dos Value Objects

Regra Descrição
Imutabilidade Depois de criado, não muda. Se precisar alterar, cria outro.
Sem identidade Não há ID. Dois objetos iguais em valor são o mesmo.
Autocontido Carrega sua própria lógica (validação, formatação, normalização etc).
Comparação por valor Igualdade baseada nos campos internos.
Pertence a uma Entidade Usado dentro de entidades ou agregados.

Retomando o exemplo do CPF. Como seria caso queiramos trocar o CPF de string para Value Object?

public class Cpf
{
    public string Numero { get; }

    public Cpf(string numero)
    {
        if (!EhValido(numero))
            throw new ArgumentException("CPF inválido.");

        Numero = Formatar(numero);
    }

    private bool EhValido(string numero)
    {
        // lógica real de validação de CPF (dígito verificador, etc)
    }

    private string Formatar(string numero)
    {
        // aplica máscara: 123.456.789-00
    }

    public override bool Equals(object obj)
    {
        if (obj is not Cpf outro) return false;
        return Numero == outro.Numero;
    }

    public override int GetHashCode() => Numero.GetHashCode();
}

Enter fullscreen mode Exit fullscreen mode

Agora na entidade:

public class Pessoa
{
    public Cpf Cpf { get; private set; }

    public Pessoa(Cpf cpf)
    {
        Cpf = cpf;
    }
}
Enter fullscreen mode Exit fullscreen mode

Agora Pessoa não sabe como validar ou formatar CPF — essa responsabilidade pertence ao próprio objeto de valor.

Quando utilizar Value Objects?

É recomendado utilizar sempre que:

  • O valor tiver regra de negócio própria
  • Não precisar de identidade única
  • Houver validação, formatação ou comportamento associado
  • Representar um conceito do domínio, não só um dado técnico

Alguns exemplos clássicos são:

  • CPF/CNPJ
  • Email
  • Endereço
  • Dinheiro (Money, com moeda e valor)
  • Período (DateRange)
  • Coordenada geográfica
  • Placa de carro

2. Entidades e Aggregates Objects

São entidades que estão em um mesmo contexto e se utilizam. É um agrupamento lógico de objetos relacionados que devem ser manipulados juntos para manter a consistência das regras dos negócios. Como já dito, o objetivo principal é garantir a integridade e consistência dos dados dentro de um Bounded Context. Ou seja, tudo que pertence ao mesmo Aggregate é corente e válido entre si. Mesmo antes de ser persistido no banco.

Pense no aggregate como uma fronteira de integridade. Dentro dela tudo deve estar consistente imediatamente (atomicamente).

Estrutura de um Aggregate

Geralmente um aggregate normalmente contém:

  1. Root Entity (Aggregate Root): a entidade principal controla o acesso e a manipulação das demais partes do grupo
  2. Entidades internas: entidades que compõe o agragado, mas que não são acessadas diretamente de fora
  3. Value Objects: objetos sem identidade própria que percentem à raíz

Exemplo: Pedido

Aggregate: Pedido
 ├── Root: Pedido
    ├── Entidade interna: ItemPedido
    └── Value Object: EnderecoEntrega
Enter fullscreen mode Exit fullscreen mode
  • O Pedido (root) é responsável por adicionar/remover itens.
  • Nenhum outro agregado (como Cliente ou Pagamento) pode alterar ItemPedido diretamente.
  • Quando o Pedido é confirmado, ele emite um evento PedidoConfirmado — que outro agregado (como Estoque) escuta e reage.

3. Repositorios

Você pode ler mais sobre o padrão repositório aqui. Trazendo-o para o contexto do DDD, a regra fundamental é que repositórios existem para os Aggregate Roots, e não para entidade internas.

Isso acontece porque o Aggregate Root é o “guardião” da consistência do agregado.

Quando você salva um agregado (pedidoRepository.Save(pedido)), o repositório persiste todo o agregado (pedido + itens + endereço).

Quando você carrega um agregado (pedidoRepository.GetById(id)), Ele retorna o aggregate root completo (com suas entidades e value objects).

Exemplo de estrutura

/Domain
  /Pedidos
    Pedido.cs
    ItemPedido.cs
    EnderecoEntrega.cs
    IPedidoRepository.cs   interface (domínio)

/Infrastructure
  /Repositories
    PedidoRepository.cs    implementação concreta (infra)
Enter fullscreen mode Exit fullscreen mode

Top comments (0)