Domain-Driven Design (DDD)

Como aplicar DDD para modelar domínios complexos, definir bounded contexts e construir software alinhado com o negócio.

Que problema resolve

Em sistemas complexos, o maior desafio não é técnico, mas sim comunicacional. Os desenvolvedores falam um idioma, os especialistas de negócio falam outro, e o software acaba sendo uma tradução defeituosa de ambos. O resultado: modelos de dados que não refletem a realidade do negócio, lógica espalhada em camadas incorretas e mudanças que exigem modificar meio sistema.

Domain-Driven Design ataca esse problema pela raiz: propõe que o software deve modelar diretamente o domínio do negócio, usando a mesma linguagem que os especialistas do domínio usam. Não é um framework nem uma biblioteca, é uma abordagem de design que coloca o domínio no centro de todas as decisões arquiteturais.

Sintomas de que você precisa de DDD

  • A equipe técnica e a equipe de negócio não se entendem
  • Os modelos de dados são tabelas de banco de dados disfarçadas de objetos
  • Uma mudança em uma regra de negócio exige modificar múltiplos serviços
  • Não há limites claros entre as responsabilidades de cada parte do sistema
  • A lógica de negócio está espalhada entre controllers, services e repositories
  • Os nomes no código não coincidem com os termos que o negócio usa

Exemplo concreto do problema

Imagine um sistema de e-commerce onde a equipe de negócio fala de “pedidos”, “devoluções” e “reembolsos”, mas o código tem classes como DataProcessor, ItemHandler e TransactionManager. Quando o negócio diz “precisamos mudar a política de devoluções”, ninguém sabe quais arquivos mexer, porque não existe uma classe Devolução nem um módulo Reembolsos.

❌ Sin DDD — Código desconectado del negocio:
   src/
   ├── handlers/
   │   ├── DataProcessor.java
   │   ├── ItemHandler.java
   │   └── TransactionManager.java
   ├── models/
   │   ├── Record.java
   │   └── Entry.java
   └── utils/
       └── Helper.java

✅ Con DDD — Código alineado con el dominio:
   src/
   ├── pedidos/
   │   ├── Pedido.java
   │   ├── LíneaDePedido.java
   │   └── RepositorioPedidos.java
   ├── devoluciones/
   │   ├── Devolución.java
   │   ├── PolíticaDevolución.java
   │   └── ServicioReembolso.java
   └── envíos/
       ├── Envío.java
       └── TransportistaAdapter.java

Como funciona

O DDD se divide em duas grandes áreas: design estratégico (como dividir o sistema) e design tático (como modelar cada parte).

Design estratégico

Ubiquitous Language (Linguagem Ubíqua)

É o vocabulário compartilhado entre desenvolvedores e especialistas do domínio. Se o negócio fala de “pedidos”, “envios” e “devoluções”, o código deve usar exatamente esses termos — e não “orders_table”, “shipping_record” ou “return_entity”.

❌ Código sin lenguaje ubicuo:
   class DataRecord { processItem() }

✅ Código con lenguaje ubicuo:
   class Pedido { confirmarEnvío() }

A linguagem ubíqua é documentada, mantida e usada em conversas, código, testes e documentação. Não é apenas uma convenção de nomes — é um acordo ativo entre negócio e desenvolvimento que evolui junto com o domínio.

Como construir a linguagem ubíqua:

  1. Organize sessões de Event Storming com especialistas do domínio e desenvolvedores
  2. Identifique os termos-chave que o negócio usa e documente-os em um glossário compartilhado
  3. Use esses termos exatos no código, nos testes, na documentação e nas conversas
  4. Quando surgir ambiguidade (a mesma palavra significando coisas diferentes), é sinal de que você precisa de bounded contexts separados

Bounded Contexts (Contextos Delimitados)

Um bounded context é um limite explícito dentro do qual um modelo de domínio tem significado consistente. A mesma palavra pode significar coisas diferentes em contextos distintos:

  • “Cliente” no contexto de Vendas: alguém que compra produtos
  • “Cliente” no contexto de Suporte: alguém que abre tickets
  • “Cliente” no contexto de Faturamento: uma entidade com dados fiscais
┌─────────────────────┐  ┌─────────────────────┐
│  Contexto: Ventas   │  │  Contexto: Soporte  │
│                     │  │                     │
│  Cliente            │  │  Cliente            │
│  ├─ nombre          │  │  ├─ nombre          │
│  ├─ historialCompra │  │  ├─ ticketsAbiertos │
│  └─ descuento       │  │  └─ nivelPrioridad  │
│                     │  │                     │
│  Pedido             │  │  Ticket             │
│  ├─ líneas          │  │  ├─ descripción     │
│  └─ total           │  │  └─ estado          │
└─────────────────────┘  └─────────────────────┘

Cada bounded context tem seu próprio modelo, seu próprio banco de dados e sua própria equipe (idealmente). A comunicação entre contextos é feita por meio de interfaces bem definidas.

Regras práticas para identificar bounded contexts:

  • Se duas equipes precisam de modelos diferentes do mesmo conceito → contextos separados
  • Se uma mudança em uma área não deveria afetar outra → contextos separados
  • Se a linguagem ubíqua muda → você está cruzando um limite de contexto
  • Um bounded context não é necessariamente um microsserviço, mas é um candidato natural a se tornar um

Context Mapping

Define como os bounded contexts se relacionam entre si:

RelaçãoDescriçãoExemplo
Shared KernelDois contextos compartilham um subconjunto do modeloTipos de moeda compartilhados entre Vendas e Faturamento
Customer-SupplierUm contexto fornece dados que outro consomeEstoque (supplier) fornece stock a Pedidos (customer)
ConformistUm contexto se adapta ao modelo de outro sem negociaçãoSeu sistema se adapta ao modelo da API de um fornecedor externo
Anti-Corruption LayerUm contexto traduz o modelo externo para sua própria linguagemAdapter que traduz respostas de um sistema legado
Open Host ServiceUm contexto expõe um protocolo aberto para que outros o consumamAPI REST pública com documentação OpenAPI
Published LanguageUsa-se uma linguagem compartilhada documentada (JSON Schema, Protobuf)Contratos de eventos no formato Avro

Design tático

Entities (Entidades)

Objetos com identidade única que persiste ao longo do tempo. Duas entidades com os mesmos atributos, mas com IDs diferentes, são diferentes.

Pedido #1234 ≠ Pedido #5678
(aunque ambos tengan los mismos productos y el mismo total)

As entidades encapsulam comportamento, não apenas dados. Um Pedido não é um DTO com getters e setters — tem métodos que refletem operações do domínio:

// ❌ Modelo anémico (anti-patrón)
pedido.setEstado("confirmado");
pedido.setFechaConfirmacion(new Date());

// ✅ Modelo rico con comportamiento
pedido.confirmar();  // internamente valida reglas y cambia estado

Value Objects (Objetos de Valor)

Objetos definidos por seus atributos, sem identidade própria. Dois value objects com os mesmos atributos são iguais. São imutáveis.

Dirección("Calle 1", "Ciudad A") == Dirección("Calle 1", "Ciudad A")
Dinero(100, "USD") == Dinero(100, "USD")

Os value objects são mais comuns do que parece. Muitos conceitos que modelamos como strings ou números primitivos deveriam ser value objects:

// ❌ Tipos primitivos
String email = "user@example.com";
double precio = 99.99;

// ✅ Value objects con validación
Email email = new Email("user@example.com");  // valida formato
Dinero precio = new Dinero(99.99, "USD");      // evita mezclar monedas

Aggregates (Agregados)

Um cluster de entidades e value objects que são tratados como uma unidade de consistência. Cada agregado tem uma raiz (Aggregate Root), que é o único ponto de acesso externo.

┌─────────────────────────────┐
│  Aggregate: Pedido          │
│  ┌───────────────────────┐  │
│  │ Pedido (Aggregate Root)│  │
│  │  ├─ id                │  │
│  │  ├─ estado            │  │
│  │  └─ fechaCreación     │  │
│  └───────────────────────┘  │
│  ┌──────────┐ ┌──────────┐  │
│  │ LíneaPed │ │ LíneaPed │  │
│  │ producto │ │ producto │  │
│  │ cantidad │ │ cantidad │  │
│  │ precio   │ │ precio   │  │
│  └──────────┘ └──────────┘  │
│  ┌──────────────────────┐   │
│  │ DirecciónEnvío (VO)  │   │
│  └──────────────────────┘   │
└─────────────────────────────┘

Regras-chave dos agregados:

  • As transações só devem modificar um agregado por vez
  • As referências entre agregados são feitas por ID, não por referência direta
  • Se você precisa modificar vários agregados, use Domain Events
  • Mantenha os agregados pequenos — um agregado grande é sinal de limites incorretos

Domain Events (Eventos de Domínio)

Representam algo que aconteceu no domínio. São expressos no passado e na linguagem do negócio.

PedidoConfirmado { pedidoId, fecha, total }
PagoRecibido { pagoId, pedidoId, monto }
EnvíoDespachado { envíoId, pedidoId, transportista }

Os eventos permitem que os bounded contexts se comuniquem de forma desacoplada. São a base da Event-Driven Architecture e do Saga Pattern para coordenar transações distribuídas.

Exemplo de fluxo com eventos:

Contexto Ventas:
  pedido.confirmar()
  → publica PedidoConfirmado

Contexto Inventario:
  escucha PedidoConfirmado
  → reserva stock
  → publica StockReservado

Contexto Envíos:
  escucha StockReservado
  → programa envío
  → publica EnvíoProgramado

Repositories (Repositórios)

Abstrações que encapsulam o acesso a dados. O domínio não sabe se os dados vêm de um banco de dados, de uma API ou de um arquivo. O repositório fala a linguagem do domínio.

❌ pedidoDAO.selectByStatusAndDate(status, date)
✅ repositorioPedidos.buscarPendientesDesde(fecha)

Domain Services (Serviços de Domínio)

Operações que não pertencem naturalmente a nenhuma entidade ou value object. Contêm lógica de negócio que envolve múltiplos agregados.

ServicioDePrecios.calcularDescuento(cliente, pedido, promoción)
ServicioDeEnvío.seleccionarTransportista(pedido, destino)

Exemplo prático: E-commerce com DDD

Vejamos como o DDD é aplicado a um sistema de e-commerce real:

Passo 1: Identificar bounded contexts

┌─────────────────┐  ┌─────────────────┐  ┌─────────────────┐
│    Catálogo      │  │    Pedidos       │  │    Pagos         │
│                  │  │                  │  │                  │
│  Producto        │  │  Pedido          │  │  Transacción     │
│  Categoría       │  │  LíneaDePedido   │  │  MétodoDePago    │
│  Precio          │  │  DirecciónEnvío  │  │  Reembolso       │
│  Inventario      │  │  EstadoPedido    │  │  EstadoPago      │
└────────┬────────┘  └────────┬────────┘  └────────┬────────┘
         │                    │                    │
         │    Eventos         │    Eventos         │
         └────────────────────┴────────────────────┘

Passo 2: Definir agregados no contexto de Pedidos

Aggregate Root: Pedido
├── id: PedidoId (Value Object)
├── cliente: ClienteId (referencia por ID, no por objeto)
├── líneas: List<LíneaDePedido>
│   ├── productoId: ProductoId
│   ├── cantidad: Cantidad (Value Object, > 0)
│   └── precioUnitario: Dinero (Value Object)
├── dirección: DirecciónEnvío (Value Object)
├── estado: EstadoPedido (enum)
└── total(): Dinero (calculado)

Invariantes del agregado:
- Un pedido debe tener al menos una línea
- El total nunca puede ser negativo
- Solo se puede confirmar un pedido en estado "borrador"
- Solo se puede cancelar un pedido que no ha sido enviado

Passo 3: Implementar comportamento no agregado

class Pedido {
  confirmar() {
    if (this.estado !== 'borrador')
      throw new Error('Solo se puede confirmar un pedido en borrador');
    if (this.líneas.length === 0)
      throw new Error('El pedido debe tener al menos una línea');

    this.estado = 'confirmado';
    this.fechaConfirmación = new Date();
    this.registrarEvento(new PedidoConfirmado(this.id, this.total()));
  }

  cancelar(motivo) {
    if (this.estado === 'enviado' || this.estado === 'entregado')
      throw new Error('No se puede cancelar un pedido ya enviado');

    this.estado = 'cancelado';
    this.motivoCancelación = motivo;
    this.registrarEvento(new PedidoCancelado(this.id, motivo));
  }
}

Vantagens

  • Alinhamento negócio-tecnologia: O código reflete diretamente o domínio, reduzindo a lacuna de comunicação
  • Limites claros: Os bounded contexts definem fronteiras explícitas que facilitam a evolução independente
  • Modelo rico: As entidades contêm comportamento, não apenas dados, evitando modelos anêmicos
  • Escalabilidade organizacional: Cada bounded context pode ser desenvolvido por uma equipe independente
  • Evolução controlada: As mudanças em um contexto não afetam os outros se as interfaces forem mantidas
  • Base para microsserviços: Os bounded contexts são candidatos naturais a se tornarem microsserviços
  • Testes mais claros: Os testes refletem regras de negócio reais, e não implementações técnicas

Trade-offs / Desvantagens

  • Curva de aprendizado alta: O DDD introduz muitos conceitos que exigem tempo para dominar
  • Overhead em domínios simples: Para CRUDs básicos, o DDD adiciona complexidade desnecessária
  • Requer acesso a especialistas do domínio: Sem colaboração constante com o negócio, o modelo se desvia
  • Mais código inicial: Entidades, value objects, repositórios e eventos exigem mais código do que uma abordagem direta
  • Risco de superengenharia: É fácil aplicar todos os padrões táticos quando você só precisa de alguns
  • Duplicação intencional: Cada bounded context pode ter sua própria representação de conceitos similares
  • Investimento em descoberta: As sessões de Event Storming e modelagem exigem tempo e participação ativa do negócio

Quando usar

  • Domínios complexos com muitas regras de negócio
  • Sistemas que evoluem com frequência devido a mudanças no negócio
  • Equipes grandes que precisam trabalhar em paralelo com limites claros
  • Projetos onde a comunicação entre negócio e desenvolvimento é crítica
  • Sistemas que estão sendo migrados de monólito para microsserviços
  • Quando o custo de um modelo incorreto é alto (finanças, saúde, logística)

Quando evitar

  • Aplicações CRUD simples sem lógica de negócio significativa
  • Protótipos ou MVPs onde a velocidade é mais importante que a estrutura
  • Equipes pequenas sem acesso a especialistas do domínio
  • Sistemas com domínios bem compreendidos e estáveis que não mudam
  • Projetos com orçamento ou tempo muito limitado para a fase de design

Tecnologias e implementações comuns

CategoriaOpções
Frameworks DDDAxon Framework (Java), EventSourcing.NetCore (C#), Ecotone (PHP)
Event SourcingEventStoreDB, Axon Server, Marten (.NET)
CQRSMediatR (.NET), Axon (Java), custom implementations
MensageriaApache Kafka, RabbitMQ, Amazon EventBridge
ModelagemEvent Storming, Domain Storytelling, Context Mapping
PersistênciaPostgreSQL, MongoDB (por bounded context)

Relação com outros padrões

O DDD é a fundação conceitual sobre a qual muitos outros padrões de arquitetura distribuída são construídos:

┌─────────────────────────────────────────────────┐
│              Domain-Driven Design                │
│         (Bounded Contexts + Ubiquitous Language) │
└──────────┬──────────┬──────────┬────────────────┘
           │          │          │
           ▼          ▼          ▼
    ┌──────────┐ ┌──────────┐ ┌──────────────┐
    │Microserv.│ │  Event   │ │    Shared    │
    │ Pattern  │ │  Driven  │ │    Kernel    │
    └──────────┘ └──────────┘ └──────────────┘
           │          │
           ▼          ▼
    ┌──────────┐ ┌──────────┐
    │   Saga   │ │  Outbox  │
    │ Pattern  │ │ Pattern  │
    └──────────┘ └──────────┘
  • Microservices Pattern: Os bounded contexts do DDD são o guia para definir os limites de cada microsserviço
  • Event-Driven Architecture: Os Domain Events do DDD são a base da comunicação assíncrona entre serviços
  • Saga Pattern: Coordena transações que cruzam múltiplos bounded contexts usando eventos e compensações
  • Outbox Pattern: Garante que os Domain Events sejam publicados de forma confiável junto com as mudanças de dados
  • Anti-Corruption Layer (ACL): Implementa a relação de Context Mapping que protege seu domínio de modelos externos
  • Shared Kernel: Implementa a relação de Context Mapping onde dois contextos compartilham um subconjunto do modelo
  • API Gateway / BFF: Fornecem pontos de entrada que podem mapear requests externos para os bounded contexts internos

Erros comuns ao aplicar DDD

ErroConsequênciaSolução
Aplicar DDD a todo o sistemaSuperengenharia em áreas simplesUse DDD apenas no core domain, CRUD para o resto
Bounded contexts grandes demaisMonólito distribuídoDivida por subdomínios reais, não por camadas técnicas
Bounded contexts pequenos demaisServiços tagarelasAgrupe funcionalidades que mudam juntas
Ignorar a linguagem ubíquaCódigo desconectado do negócioSessões regulares com especialistas do domínio
Modelo anêmicoLógica espalhada em servicesMova o comportamento para as entidades
Agregados grandes demaisProblemas de concorrênciaMantenha os agregados pequenos, use eventos entre eles

Próximos passos

Com o DDD como base para modelar domínios complexos, o próximo passo natural é entender como lidar com transações que cruzam múltiplos bounded contexts. Para isso existe o Saga Pattern, que coordena operações distribuídas com compensações. Explore também o Microservices Pattern para entender como os bounded contexts se transformam em serviços independentes.