O que é DDD (Domain-Driven Design)?
Linguagem ubíqua, agregados e contextos delimitados: aprenda a modelar software a partir do domínio do negócio com Domain-Driven Design, seus padrões táticos e estratégicos, e quando (não) aplicá-los.

A maior dificuldade em construir software complexo raramente está na tecnologia: está em entender o problema do negócio e traduzi-lo fielmente para código. Domain-Driven Design (DDD) é uma abordagem que coloca o domínio do negócio no centro do design, alinhando o vocabulário do código ao vocabulário dos especialistas. Neste artigo você vai conhecer os conceitos centrais do DDD — linguagem ubíqua, agregados, contextos delimitados — entender como eles se encaixam, ver código concreto em Python e descobrir quando vale (e quando não vale) a pena aplicá-los.
O que é Domain-Driven Design?
Domain-Driven Design é uma abordagem de desenvolvimento de software apresentada por Eric Evans em seu livro de 2003 (Evans, 2003). A premissa central é que o coração do software está no domínio — a área de conhecimento e atividade em torno da qual o sistema gira — e que o design deve refletir profundamente esse domínio.
DDD parte de uma constatação: em sistemas complexos, o maior risco não é técnico, é a complexidade do domínio. Por isso, em vez de começar pelo banco de dados ou pela interface, o DDD começa por compreender e modelar o negócio em estreita colaboração com quem o conhece — os especialistas do domínio (domain experts).
Vale separar o que o DDD é do que ele não é. DDD não é um framework, não é uma estrutura de pastas e não é sinônimo de "usar repositórios". É, antes de tudo, uma disciplina de modelagem colaborativa: você conversa com quem entende o negócio, destila esse entendimento em um modelo e mantém esse modelo vivo no código. As ferramentas técnicas (entidades, agregados, repositórios) existem para sustentar o modelo, não o contrário.
É útil dividir o DDD em duas frentes:
Um erro frequente de quem está começando é mergulhar direto no tático (porque é a parte com código) e ignorar o estratégico. Na prática, o estratégico costuma trazer mais retorno: errar as fronteiras de um sistema é muito mais caro do que escolher mal entre uma entidade e um value object.
Domínio, subdomínio e núcleo
Dentro de um mesmo negócio nem tudo tem a mesma importância. O DDD distingue:
Reconhecer onde está o núcleo evita o desperdício de aplicar modelagem sofisticada em partes triviais e, ao mesmo tempo, garante atenção onde ela realmente importa.
Linguagem ubíqua: o vocabulário compartilhado
O conceito mais fundamental e transformador do DDD é a linguagem ubíqua (ubiquitous language). É um vocabulário comum, construído em conjunto por desenvolvedores e especialistas do domínio, usado em todos os lugares: nas conversas, na documentação e — crucialmente — no código.
A ideia é eliminar a tradução. Se o especialista fala em "apólice", "sinistro" e "cobertura", essas mesmas palavras devem aparecer como classes, métodos e variáveis no código. Quando o desenvolvedor diz "registro" e o especialista diz "apólice" para a mesma coisa, há um espaço para mal-entendidos que vira bug.
# Código sem linguagem ubíqua: genérico e desconectado do negócio
def processar(self, item, valor):
self.registros.append({"item": item, "valor": valor})
# Código com linguagem ubíqua: fala a língua do domínio
def registrar_sinistro(self, apolice: Apolice, valor: Dinheiro) -> Sinistro:
sinistro = Sinistro.abrir(apolice, valor)
self.sinistros.adicionar(sinistro)
return sinistroRepare na diferença de leitura. O primeiro método não conta história nenhuma: "processar" o quê? Que "item"? Já o segundo é praticamente uma frase em português — qualquer pessoa do negócio consegue ler registrar_sinistro(apolice, valor) e confirmar se o comportamento está correto. Esse é o ganho prático da linguagem ubíqua: ela transforma o código em um documento revisável por quem não programa.
A linguagem ubíqua não é definida de uma vez: ela é refinada continuamente à medida que o entendimento do domínio amadurece. Mudanças no vocabulário devem se refletir no código, e vice-versa. Quando um especialista corrige você — "isso não é um cancelamento, é uma rescisão" — essa correção precisa virar um rename no código, não apenas uma anotação esquecida em ata de reunião.
Como construir a linguagem ubíqua na prática
Algumas técnicas ajudam a destilar o vocabulário:
Os blocos de construção do DDD tático
O DDD tático oferece padrões para estruturar o modelo de domínio em código. Eles se encaixam muito bem dentro da camada de domínio de uma Arquitetura Limpa, onde as regras de negócio ficam isoladas de frameworks e infraestrutura.
Entidades
Entidades são objetos definidos por sua identidade, não por seus atributos. Um Cliente continua sendo o mesmo cliente mesmo que mude de nome ou endereço, porque tem um identificador único e contínuo no tempo.
A consequência prática é que a igualdade de entidades é comparada por identidade, não por atributos. Dois clientes com o mesmo nome são pessoas diferentes; o mesmo cliente em dois momentos diferentes da vida é a mesma entidade.
class Cliente:
def __init__(self, id, nome: str):
self._id = id
self._nome = nome
def renomear(self, novo_nome: str) -> None:
if not novo_nome.strip():
raise ValueError("nome não pode ser vazio")
self._nome = novo_nome
def __eq__(self, outro) -> bool:
return isinstance(outro, Cliente) and self._id == outro._id
def __hash__(self) -> int:
return hash(self._id)Value Objects (objetos de valor)
Value objects são definidos por seus atributos, não por identidade. Dois objetos Dinheiro(100, "BRL") são intercambiáveis — não importa "qual" instância é qual. Eles costumam ser imutáveis e encapsulam validações e comportamentos relacionados ao valor.
class Dinheiro:
def __init__(self, quantia: int, moeda: str):
if quantia < 0:
raise ValueError("quantia não pode ser negativa")
self._quantia = quantia
self._moeda = moeda
def somar(self, outro: "Dinheiro") -> "Dinheiro":
if self._moeda != outro._moeda:
raise ValueError("moedas diferentes")
return Dinheiro(self._quantia + outro._quantia, self._moeda)Note duas decisões importantes nesse value object. Primeiro, ele é imutável: somar não altera o objeto, retorna um novo. Isso elimina toda uma classe de bugs por compartilhamento de estado. Segundo, ele protege suas invariantes no construtor: é impossível existir um Dinheiro com quantia negativa. Quando você modela valores assim, validações deixam de estar espalhadas pelo sistema e passam a viver em um único lugar.
Value objects são também o melhor lugar para combater obsessão por primitivos (primitive obsession): em vez de passar str para e-mail, CPF e moeda por toda parte, crie Email, Cpf e Dinheiro. O tipo passa a carregar significado e garantias.
Agregados e raízes de agregado
Um agregado é um conjunto de objetos (entidades e value objects) tratado como uma unidade de consistência. Ele tem uma raiz de agregado (aggregate root), que é a única porta de entrada para o agregado: objetos externos só podem referenciar a raiz, nunca os objetos internos diretamente.
Por exemplo, um Pedido (raiz) controla seus ItensDePedido. Você não adiciona um item manipulando a lista interna; você chama pedido.adicionar_item(...), e a raiz garante as invariantes (como "o total deve bater com a soma dos itens"). Isso protege a consistência das regras de negócio.
class Pedido: # raiz de agregado
def __init__(self, id):
self._id = id
self._itens = []
def adicionar_item(self, produto, quantidade):
if quantidade <= 0:
raise ValueError("quantidade inválida")
self._itens.append(ItemDePedido(produto, quantidade))
@property
def total(self) -> Dinheiro:
return sum((item.subtotal for item in self._itens), Dinheiro(0, "BRL"))Duas regras de ouro sobre o desenho de agregados, defendidas por Vaughn Vernon (Vernon, 2013):
A regra de consistência é direta: uma transação deve modificar um único agregado. Quando vários agregados precisam reagir a uma mudança, isso é feito por consistência eventual — tipicamente via eventos de domínio (veja a seguir), não dentro da mesma transação.
Eventos de domínio
Um evento de domínio representa algo relevante que aconteceu no domínio, no passado. Ele é nomeado no particípio ("Sinistro Aberto", "Pedido Confirmado") e carrega os dados necessários para que outras partes do sistema reajam. Eventos são a cola que permite agregados pequenos cooperarem sem acoplamento direto.
from dataclasses import dataclass
from datetime import datetime
@dataclass(frozen=True)
class PedidoConfirmado:
pedido_id: str
total: Dinheiro
ocorrido_em: datetimeA raiz de agregado registra o evento ao executar a operação, e a infraestrutura cuida de publicá-lo (por exemplo, em um message broker) depois que a transação é confirmada. Outros contextos — estoque, faturamento, notificação — assinam o evento e reagem por conta própria. Esse padrão é a base para integrar microsserviços sem acoplá-los no banco de dados.
Fábricas (factories)
Quando a criação de um agregado é complexa — envolve várias validações, monta value objects internos ou precisa garantir invariantes desde o nascimento —, essa lógica não deveria poluir o construtor nem ficar espalhada nos chamadores. Ela vira uma fábrica.
class FabricaDeSinistro:
@staticmethod
def abrir(apolice: "Apolice", valor: Dinheiro, hoje: datetime) -> "Sinistro":
if not apolice.esta_vigente(hoje):
raise ValueError("apólice fora de vigência")
if valor.maior_que(apolice.limite_de_cobertura):
raise ValueError("valor excede a cobertura")
return Sinistro(id=novo_id(), apolice_id=apolice.id, valor=valor)A fábrica concentra a regra "como nasce um sinistro válido" em um único ponto, expressando a linguagem ubíqua (abrir) e garantindo que nunca exista um sinistro em estado inconsistente.
Repositórios
Repositórios fornecem a ilusão de uma coleção em memória de agregados, escondendo os detalhes de persistência. O domínio pede repositorio.buscar_por_id(...) sem saber se por trás há SQL, NoSQL ou um arquivo. Esse isolamento depende fortemente de injeção de dependência: o domínio define a interface do repositório, e a implementação concreta é fornecida de fora.
from abc import ABC, abstractmethod
class RepositorioDePedidos(ABC): # interface no domínio
@abstractmethod
def buscar_por_id(self, pedido_id: str) -> "Pedido | None": ...
@abstractmethod
def salvar(self, pedido: "Pedido") -> None: ...A implementação concreta — RepositorioDePedidosPostgres, por exemplo — vive na camada de infraestrutura e implementa essa interface. Com isso, a lógica de domínio pode ser testada com um repositório em memória, sem banco de dados, e a tecnologia de persistência pode mudar sem tocar nas regras de negócio. Regra prática: há um repositório por agregado, e ele sempre devolve o agregado inteiro, em estado consistente — nunca pedaços soltos.
Serviços de domínio
Quando uma operação importante do domínio não pertence naturalmente a nenhuma entidade ou value object, ela vira um serviço de domínio — uma operação sem estado que expressa uma regra de negócio (por exemplo, uma transferência entre duas contas).
A pergunta-chave é: "esse comportamento é de uma entidade?" Transferir dinheiro envolve duas contas; não cabe bem em nenhuma delas isoladamente. Por isso vira um serviço. O cuidado aqui é não exagerar: se quase tudo vira serviço de domínio, suas entidades provavelmente estão anêmicas (veja "Erros comuns").
Dica: não confunda serviço de domínio com serviço de aplicação. O serviço de aplicação orquestra um caso de uso (abre transação, chama o repositório, dispara eventos) e não contém regra de negócio. O serviço de domínio contém regra de negócio, mas não conhece transações nem infraestrutura.
Contextos delimitados: o coração do DDD estratégico
Em um sistema grande, tentar criar um único modelo para tudo é uma receita para o caos: a palavra "cliente" significa coisas diferentes para o setor de vendas, de cobrança e de suporte. O DDD resolve isso com contextos delimitados (bounded contexts).
Um contexto delimitado é uma fronteira explícita dentro da qual um modelo específico — e sua linguagem ubíqua — é coerente e válido. Dentro do contexto de "Vendas", "Cliente" tem um significado e um conjunto de atributos; dentro de "Cobrança", "Cliente" pode ser modelado de forma diferente. Cada contexto tem seu próprio modelo, e a tradução entre eles é explícita.
Essa divisão é uma das pontes mais naturais entre DDD e microsserviços: um contexto delimitado costuma ser um ótimo candidato para virar um serviço independente, com seu próprio modelo, banco de dados e equipe. O alinhamento entre fronteiras de contexto e fronteiras de serviço evita o acoplamento que torna sistemas distribuídos frágeis.
Atenção a uma armadilha clássica: contexto delimitado não é o mesmo que microsserviço. Um contexto pode conter mais de um serviço, e dividir microsserviços em fronteiras erradas (técnicas, e não de domínio) gera o pior dos mundos — um monólito distribuído, com todo o acoplamento de um monólito e toda a latência de uma rede.
Mapa de contexto
O mapa de contexto (context map) documenta como os diferentes contextos delimitados se relacionam: quem depende de quem, onde há tradução de modelos, quais são parceiros e quais são fornecedor/cliente. Ele dá uma visão estratégica das integrações do sistema.
Alguns padrões de relacionamento que costumam aparecer no mapa:
Camada anticorrupção (ACL)
A camada anticorrupção merece destaque porque é uma das ferramentas mais úteis ao integrar com sistemas legados ou de terceiros. Em vez de deixar o modelo externo "vazar" para dentro do seu domínio, você cria uma camada de tradução que converte o vocabulário deles no seu.
class GatewayDeCobranca:
"""Anti-corruption layer: traduz o modelo do ERP legado
para a linguagem ubíqua do nosso domínio."""
def __init__(self, cliente_erp):
self._erp = cliente_erp
def emitir_fatura(self, pedido: "Pedido") -> "Fatura":
# o ERP fala "doc", "amt", "cur"; nós falamos Fatura/Dinheiro
resposta = self._erp.create_doc(
amount=pedido.total.quantia_em_centavos,
currency=pedido.total.moeda,
)
return Fatura(numero=resposta["doc_id"], valor=pedido.total)Sem a ACL, os termos estranhos do ERP (doc, amt, cur) acabariam infiltrados no seu código, corroendo a linguagem ubíqua que você lutou para construir.
DDD, padrões e princípios de design
Na implementação, o DDD se apoia em muitos design patterns essenciais: Repository para persistência, Factory para criação de agregados complexos, Strategy para variar regras de negócio, Observer/eventos de domínio para comunicar mudanças.
Os blocos de construção também conversam diretamente com os princípios do SOLID. Agregados com responsabilidades bem definidas refletem o princípio da responsabilidade única; repositórios definidos como abstrações e implementados na infraestrutura aplicam a inversão de dependência. Martin Fowler, ao catalogar padrões de arquitetura corporativa, descreve o Domain Model como um modelo de objetos do domínio que incorpora tanto comportamento quanto dados (Fowler, 2002) — exatamente o oposto de um modelo anêmico, que só guarda dados sem comportamento.
Vale também situar o DDD ao lado de duas abordagens que costumam aparecer junto dele. CQRS (separar o modelo de escrita do modelo de leitura) ajuda quando consultas complexas pressionam o modelo de domínio; Event Sourcing (persistir a sequência de eventos em vez do estado atual) combina naturalmente com eventos de domínio. Nenhum dos dois é obrigatório no DDD — são opções avançadas que só compensam quando o problema realmente as pede.
Quando usar (e quando não usar) DDD
O DDD tem custo: exige colaboração intensa com especialistas, modelagem cuidadosa e disciplina. Ele compensa quando:
Ele tende a ser exagero quando:
Nesses casos, aplicar todo o aparato tático do DDD adiciona complexidade sem retorno. Um software de cadastro simples raramente precisa de agregados elaborados — às vezes basta uma boa API e um modelo direto.
Uma estratégia equilibrada para times que estão começando é adotar o DDD por partes. Use o DDD estratégico (linguagem ubíqua e contextos delimitados) em todo o sistema, porque é barato e quase sempre vale. Reserve o aparato tático completo (agregados, eventos, fábricas) para o núcleo — onde a complexidade está concentrada — e mantenha os subdomínios genéricos e de suporte simples, talvez com um CRUD honesto.
Erros comuns
Perguntas frequentes
DDD é só para microsserviços? Não. DDD nasceu antes da onda de microsserviços e funciona perfeitamente em um monólito bem modelado — um monólito modular, com contextos delimitados como módulos internos, é uma excelente aplicação de DDD. Os contextos é que, mais tarde, podem virar serviços, se houver motivo.
DDD exige orientação a objetos? Não obrigatoriamente. Os conceitos mapeiam bem para OO, mas value objects imutáveis, eventos e funções puras também se expressam muito bem em estilo funcional. O essencial é o modelo e a linguagem, não o paradigma.
Qual a diferença entre entidade e value object, na prática? Pergunte: "se dois desses objetos tiverem os mesmos atributos, eles são a mesma coisa?" Se sim, é value object (dois Dinheiro(10, "BRL") são iguais). Se você precisa distingui-los e rastreá-los ao longo do tempo, é entidade (dois clientes homônimos são pessoas diferentes).
Posso adotar DDD aos poucos? Sim, e geralmente é o melhor caminho. Comece pela linguagem ubíqua e por identificar contextos. Introduza agregados e eventos só onde a complexidade justifica. Refatorar em direção ao DDD é uma jornada, não um big bang.
DDD e Clean Architecture são a mesma coisa? São complementares. A Arquitetura Limpa diz onde colocar as coisas (camadas, dependências apontando para dentro); o DDD diz o que colocar na camada de domínio e como modelá-la. Juntos formam uma combinação muito usada.
Conclusão
Domain-Driven Design é, antes de tudo, uma forma de levar o negócio a sério dentro do código. Com a linguagem ubíqua, desenvolvedores e especialistas falam a mesma língua; com agregados, value objects, fábricas e repositórios, o domínio ganha estrutura e consistência; com eventos de domínio, as partes cooperam sem se acoplar; e com contextos delimitados e mapas de contexto, sistemas grandes são divididos em partes coerentes e evolutivas. O DDD não é gratuito e não serve para todo projeto, mas em domínios complexos e duradouros ele é uma das abordagens mais eficazes para manter o software alinhado ao negócio ao longo do tempo. Comece pela linguagem ubíqua e pelos contextos, invista o aparato tático onde está o núcleo — o resto se constrói a partir daí.