DDD: quando o código fala a língua do negócio

23 de fevereiro de 2026

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.

GitHub
LinkedIn