Você já precisou explicar o sistema para alguém do negócio e sentiu que estavam falando idiomas diferentes? O dev falando em tabelas e endpoints, o cliente falando em consultas e prontuários? O DDD existe para acabar com essa barreira.
O que é DDD?
Domain-Driven Design (Design Orientado ao Domínio) é uma abordagem criada por Eric Evans que coloca o domínio do negócio no centro de todas as decisões de software.
A ideia central é simples: o código deve refletir a linguagem e os conceitos do negócio — não o contrário. O sistema não tem "tabela animal" e "insert no banco". Ele tem Animal, Consulta, Agendamento.
Ubiquitous Language — a língua em comum
O primeiro e mais importante conceito do DDD é a Linguagem Ubíqua (Ubiquitous Language): uma linguagem compartilhada entre desenvolvedores e especialistas do negócio.
Todo mundo usa os mesmos termos. O que o veterinário chama de "prontuário", o código também chama de Prontuario. O que o atendente chama de "agendamento", a classe também se chama Agendamento.
Isso elimina a tradução constante entre o mundo do negócio e o mundo do código — e elimina junto boa parte dos bugs que nascem dessa tradução errada.
Os blocos de construção do DDD
Entities — tem identidade própria
Uma Entity é um objeto que tem identidade única — ela não é definida pelos seus atributos, mas pelo seu identificador.
A Maya continua sendo a Maya mesmo que ela mude de peso, de pelagem ou de dono. Ela tem um id que a define.
from dataclasses import dataclass from uuid import UUID @dataclass class Animal: id: UUID nome: str especie: str raca: str def __eq__(self, other: object) -> bool: if not isinstance(other, Animal): return False return self.id == other.id # identidade pelo id, não pelos atributos
Value Objects — definido pelos atributos
Um Value Object não tem identidade. Dois objetos com os mesmos atributos são considerados iguais. Peso, endereço, data — esses são Value Objects.
@dataclass(frozen=True) # imutável por natureza class Peso: valor: float unidade: str # "kg" def __post_init__(self) -> None: if self.valor <= 0: raise ValueError("Peso deve ser positivo") # Dois pesos iguais são a mesma coisa peso_maya = Peso(valor=3.2, unidade="kg") peso_clone = Peso(valor=3.2, unidade="kg") print(peso_maya == peso_clone) # True
Aggregates — o guardião da consistência
Um Aggregate é um grupo de objetos tratados como uma unidade. Ele tem uma raiz (Aggregate Root) que é o único ponto de entrada para modificações.
No petshop, um Prontuario agrupa todas as Consultas de um animal. Ninguém adiciona uma consulta diretamente — sempre passa pelo Prontuario.
from dataclasses import dataclass, field from datetime import date from typing import List @dataclass class Consulta: data: date descricao: str veterinario: str @dataclass class Prontuario: # Aggregate Root animal_id: UUID consultas: List[Consulta] = field(default_factory=list) def adicionar_consulta(self, consulta: Consulta) -> None: self.consultas.append(consulta) def ultima_consulta(self) -> Consulta | None: return self.consultas[-1] if self.consultas else None
Repositories — abstração do acesso a dados
O Repository é a camada que isola o domínio do banco de dados. O domínio não sabe se os dados estão no PostgreSQL, MongoDB ou em memória — ele só pede e recebe.
from abc import ABC, abstractmethod class ProntuarioRepository(ABC): @abstractmethod def buscar_por_animal(self, animal_id: UUID) -> Prontuario | None: pass @abstractmethod def salvar(self, prontuario: Prontuario) -> None: pass # A implementação concreta fica fora do domínio class ProntuarioPostgresRepository(ProntuarioRepository): def buscar_por_animal(self, animal_id: UUID) -> Prontuario | None: pass # query no banco aqui def salvar(self, prontuario: Prontuario) -> None: pass # insert/update aqui
Domain Services — lógica que não pertence a ninguém
Às vezes uma regra de negócio não pertence naturalmente a uma Entity nem a um Value Object. Ela envolve múltiplos objetos. Aí entra o Domain Service.
class AgendadorDeConsultas: def __init__(self, prontuario_repo: ProntuarioRepository): self._prontuario_repo = prontuario_repo def agendar(self, animal_id: UUID, consulta: Consulta) -> None: prontuario = self._prontuario_repo.buscar_por_animal(animal_id) if prontuario is None: prontuario = Prontuario(animal_id=animal_id) prontuario.adicionar_consulta(consulta) self._prontuario_repo.salvar(prontuario)
Bounded Contexts — cada domínio no seu quadrado
Em sistemas maiores, o mesmo conceito pode ter significados diferentes para áreas diferentes. Para o veterinário, Animal tem espécie, raça e prontuário. Para o financeiro, Animal tem dono e histórico de pagamentos.
O DDD resolve isso com Bounded Contexts: fronteiras explícitas onde cada domínio tem seu próprio modelo, sem conflito com os demais.
┌──────────────────────┐ ┌───────────────────────┐ │ Contexto Clínico │ │ Contexto Financeiro │ │ │ │ │ │ Animal │ │ Animal │ │ • especie │ │ • dono │ │ • raca │ │ • plano │ │ • prontuario │ │ • inadimplente │ └──────────────────────┘ └───────────────────────┘
Mesmo nome, modelos diferentes — e tudo bem, porque estão em contextos separados.
DDD + Clean Architecture + Microsserviços
Se você leu os posts anteriores da série, vai perceber que tudo se conecta:
- As Entities e Value Objects do DDD vivem na camada de Entities da Clean Architecture
- Os Use Cases da Clean Architecture orquestram os Domain Services do DDD
- Cada Bounded Context do DDD é um candidato natural a virar um Microsserviço
Não são concorrentes — são complementares.
Quando usar DDD?
DDD brilha em sistemas com domínio complexo — muitas regras de negócio, muitos especialistas envolvidos, muita lógica que muda com frequência.
Para um CRUD simples, pode ser excessivo. Mas se o seu sistema resolve problemas complexos do mundo real, DDD é o mapa que vai manter você e o time orientados enquanto o sistema cresce.