Domain-Driven Design (DDD)
Cómo aplicar DDD para modelar dominios complejos, definir bounded contexts y construir software alineado con el negocio.
Qué problema resuelve
En sistemas complejos, el mayor desafío no es técnico sino comunicacional. Los desarrolladores hablan un idioma, los expertos de negocio hablan otro, y el software termina siendo una traducción defectuosa de ambos. El resultado: modelos de datos que no reflejan la realidad del negocio, lógica dispersa en capas incorrectas y cambios que requieren modificar medio sistema.
Domain-Driven Design ataca este problema desde la raíz: propone que el software debe modelar directamente el dominio del negocio, usando el mismo lenguaje que usan los expertos del dominio. No es un framework ni una librería, es un enfoque de diseño que pone el dominio en el centro de todas las decisiones arquitectónicas.
Síntomas de que necesitas DDD
- El equipo técnico y el equipo de negocio no se entienden
- Los modelos de datos son tablas de base de datos disfrazadas de objetos
- Un cambio en una regla de negocio requiere modificar múltiples servicios
- No hay límites claros entre las responsabilidades de cada parte del sistema
- La lógica de negocio está dispersa entre controladores, servicios y repositorios
- Los nombres en el código no coinciden con los términos que usa el negocio
Ejemplo concreto del problema
Imagina un sistema de e-commerce donde el equipo de negocio habla de “pedidos”, “devoluciones” y “reembolsos”, pero el código tiene clases como DataProcessor, ItemHandler y TransactionManager. Cuando el negocio dice “necesitamos cambiar la política de devoluciones”, nadie sabe qué archivos tocar porque no hay una clase Devolución ni un 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
Cómo funciona
DDD se divide en dos grandes áreas: diseño estratégico (cómo dividir el sistema) y diseño táctico (cómo modelar cada parte).
Diseño estratégico
Ubiquitous Language (Lenguaje Ubicuo)
Es el vocabulario compartido entre desarrolladores y expertos del dominio. Si el negocio habla de “pedidos”, “envíos” y “devoluciones”, el código debe usar exactamente esos términos — no “orders_table”, “shipping_record” o “return_entity”.
❌ Código sin lenguaje ubicuo:
class DataRecord { processItem() }
✅ Código con lenguaje ubicuo:
class Pedido { confirmarEnvío() }
El lenguaje ubicuo se documenta, se mantiene y se usa en conversaciones, código, tests y documentación. No es solo una convención de nombres — es un acuerdo activo entre negocio y desarrollo que evoluciona con el dominio.
Cómo construir el lenguaje ubicuo:
- Organiza sesiones de Event Storming con expertos del dominio y desarrolladores
- Identifica los términos clave que usa el negocio y documéntalos en un glosario compartido
- Usa esos términos exactos en el código, los tests, la documentación y las conversaciones
- Cuando surja ambigüedad (la misma palabra significa cosas diferentes), es señal de que necesitas bounded contexts separados
Bounded Contexts (Contextos Delimitados)
Un bounded context es un límite explícito dentro del cual un modelo de dominio tiene significado consistente. La misma palabra puede significar cosas diferentes en contextos distintos:
- “Cliente” en el contexto de Ventas: alguien que compra productos
- “Cliente” en el contexto de Soporte: alguien que abre tickets
- “Cliente” en el contexto de Facturación: una entidad con datos fiscales
┌─────────────────────┐ ┌─────────────────────┐
│ Contexto: Ventas │ │ Contexto: Soporte │
│ │ │ │
│ Cliente │ │ Cliente │
│ ├─ nombre │ │ ├─ nombre │
│ ├─ historialCompra │ │ ├─ ticketsAbiertos │
│ └─ descuento │ │ └─ nivelPrioridad │
│ │ │ │
│ Pedido │ │ Ticket │
│ ├─ líneas │ │ ├─ descripción │
│ └─ total │ │ └─ estado │
└─────────────────────┘ └─────────────────────┘
Cada bounded context tiene su propio modelo, su propia base de datos y su propio equipo (idealmente). La comunicación entre contextos se hace a través de interfaces bien definidas.
Reglas prácticas para identificar bounded contexts:
- Si dos equipos necesitan modelos diferentes del mismo concepto → contextos separados
- Si un cambio en un área no debería afectar a otra → contextos separados
- Si el lenguaje ubicuo cambia → estás cruzando un límite de contexto
- Un bounded context no es necesariamente un microservicio, pero es un candidato natural para serlo
Context Mapping
Define cómo se relacionan los bounded contexts entre sí:
| Relación | Descripción | Ejemplo |
|---|---|---|
| Shared Kernel | Dos contextos comparten un subconjunto del modelo | Tipos de moneda compartidos entre Ventas y Facturación |
| Customer-Supplier | Un contexto provee datos que otro consume | Inventario (supplier) provee stock a Pedidos (customer) |
| Conformist | Un contexto se adapta al modelo de otro sin negociación | Tu sistema se adapta al modelo de la API de un proveedor externo |
| Anti-Corruption Layer | Un contexto traduce el modelo externo a su propio lenguaje | Adapter que traduce respuestas de un sistema legacy |
| Open Host Service | Un contexto expone un protocolo abierto para que otros lo consuman | API REST pública con documentación OpenAPI |
| Published Language | Se usa un lenguaje compartido documentado (JSON Schema, Protobuf) | Contratos de eventos en formato Avro |
Diseño táctico
Entities (Entidades)
Objetos con identidad única que persiste a lo largo del tiempo. Dos entidades con los mismos atributos pero diferente ID son diferentes.
Pedido #1234 ≠ Pedido #5678
(aunque ambos tengan los mismos productos y el mismo total)
Las entidades encapsulan comportamiento, no solo datos. Un Pedido no es un DTO con getters y setters — tiene métodos que reflejan operaciones del dominio:
// ❌ 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 sus atributos, sin identidad propia. Dos value objects con los mismos atributos son iguales. Son inmutables.
Dirección("Calle 1", "Ciudad A") == Dirección("Calle 1", "Ciudad A")
Dinero(100, "USD") == Dinero(100, "USD")
Los value objects son más comunes de lo que parece. Muchos conceptos que modelamos como strings o números primitivos deberían 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)
Un cluster de entidades y value objects que se tratan como una unidad de consistencia. Cada agregado tiene una raíz (Aggregate Root) que es el único punto de acceso externo.
┌─────────────────────────────┐
│ Aggregate: Pedido │
│ ┌───────────────────────┐ │
│ │ Pedido (Aggregate Root)│ │
│ │ ├─ id │ │
│ │ ├─ estado │ │
│ │ └─ fechaCreación │ │
│ └───────────────────────┘ │
│ ┌──────────┐ ┌──────────┐ │
│ │ LíneaPed │ │ LíneaPed │ │
│ │ producto │ │ producto │ │
│ │ cantidad │ │ cantidad │ │
│ │ precio │ │ precio │ │
│ └──────────┘ └──────────┘ │
│ ┌──────────────────────┐ │
│ │ DirecciónEnvío (VO) │ │
│ └──────────────────────┘ │
└─────────────────────────────┘
Reglas clave de los agregados:
- Las transacciones solo deben modificar un agregado a la vez
- Las referencias entre agregados se hacen por ID, no por referencia directa
- Si necesitas modificar varios agregados, usa Domain Events
- Mantén los agregados pequeños — un agregado grande es señal de límites incorrectos
Domain Events (Eventos de Dominio)
Representan algo que ocurrió en el dominio. Se expresan en pasado y en lenguaje del negocio.
PedidoConfirmado { pedidoId, fecha, total }
PagoRecibido { pagoId, pedidoId, monto }
EnvíoDespachado { envíoId, pedidoId, transportista }
Los eventos permiten que los bounded contexts se comuniquen de forma desacoplada. Son la base de la Event-Driven Architecture y del Saga Pattern para coordinar transacciones distribuidas.
Ejemplo de flujo con 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 (Repositorios)
Abstracciones que encapsulan el acceso a datos. El dominio no sabe si los datos vienen de una base de datos, una API o un archivo. El repositorio habla el lenguaje del dominio.
❌ pedidoDAO.selectByStatusAndDate(status, date)
✅ repositorioPedidos.buscarPendientesDesde(fecha)
Domain Services (Servicios de Dominio)
Operaciones que no pertenecen naturalmente a ninguna entidad o value object. Contienen lógica de negocio que involucra múltiples agregados.
ServicioDePrecios.calcularDescuento(cliente, pedido, promoción)
ServicioDeEnvío.seleccionarTransportista(pedido, destino)
Ejemplo práctico: E-commerce con DDD
Veamos cómo se aplica DDD a un sistema de e-commerce real:
Paso 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 │
└────────────────────┴────────────────────┘
Paso 2: Definir agregados en el 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
Paso 3: Implementar comportamiento en el 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));
}
}
Ventajas
- Alineación negocio-tecnología: El código refleja directamente el dominio, reduciendo la brecha de comunicación
- Límites claros: Los bounded contexts definen fronteras explícitas que facilitan la evolución independiente
- Modelo rico: Las entidades contienen comportamiento, no solo datos, evitando modelos anémicos
- Escalabilidad organizacional: Cada bounded context puede ser desarrollado por un equipo independiente
- Evolución controlada: Los cambios en un contexto no afectan a otros si las interfaces se mantienen
- Base para microservicios: Los bounded contexts son candidatos naturales para convertirse en microservicios
- Testing más claro: Los tests reflejan reglas de negocio reales, no implementaciones técnicas
Trade-offs / Desventajas
- Curva de aprendizaje alta: DDD introduce muchos conceptos que requieren tiempo para dominar
- Overhead en dominios simples: Para CRUDs básicos, DDD añade complejidad innecesaria
- Requiere acceso a expertos del dominio: Sin colaboración constante con el negocio, el modelo se desvía
- Más código inicial: Entidades, value objects, repositorios y eventos requieren más código que un enfoque directo
- Riesgo de sobre-ingeniería: Es fácil aplicar todos los patrones tácticos cuando solo necesitas algunos
- Duplicación intencional: Cada bounded context puede tener su propia representación de conceptos similares
- Inversión en descubrimiento: Las sesiones de Event Storming y modelado requieren tiempo y participación activa del negocio
Cuándo usar
- Dominios complejos con muchas reglas de negocio
- Sistemas que evolucionan frecuentemente por cambios en el negocio
- Equipos grandes que necesitan trabajar en paralelo con límites claros
- Proyectos donde la comunicación entre negocio y desarrollo es crítica
- Sistemas que se están migrando de monolito a microservicios
- Cuando el costo de un modelo incorrecto es alto (finanzas, salud, logística)
Cuándo evitar
- Aplicaciones CRUD simples sin lógica de negocio significativa
- Prototipos o MVPs donde la velocidad es más importante que la estructura
- Equipos pequeños sin acceso a expertos del dominio
- Sistemas con dominios bien entendidos y estables que no cambian
- Proyectos con presupuesto o tiempo muy limitado para la fase de diseño
Tecnologías e implementaciones comunes
| Categoría | Opciones |
|---|---|
| Frameworks DDD | Axon Framework (Java), EventSourcing.NetCore (C#), Ecotone (PHP) |
| Event Sourcing | EventStoreDB, Axon Server, Marten (.NET) |
| CQRS | MediatR (.NET), Axon (Java), custom implementations |
| Mensajería | Apache Kafka, RabbitMQ, Amazon EventBridge |
| Modelado | Event Storming, Domain Storytelling, Context Mapping |
| Persistencia | PostgreSQL, MongoDB (por bounded context) |
Relación con otros patrones
DDD es el fundamento conceptual sobre el que se construyen muchos otros patrones de arquitectura distribuida:
┌─────────────────────────────────────────────────┐
│ Domain-Driven Design │
│ (Bounded Contexts + Ubiquitous Language) │
└──────────┬──────────┬──────────┬────────────────┘
│ │ │
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────────┐
│Microserv.│ │ Event │ │ Shared │
│ Pattern │ │ Driven │ │ Kernel │
└──────────┘ └──────────┘ └──────────────┘
│ │
▼ ▼
┌──────────┐ ┌──────────┐
│ Saga │ │ Outbox │
│ Pattern │ │ Pattern │
└──────────┘ └──────────┘
- Microservices Pattern: Los bounded contexts de DDD son la guía para definir los límites de cada microservicio
- Event-Driven Architecture: Los Domain Events de DDD son la base de la comunicación asíncrona entre servicios
- Saga Pattern: Coordina transacciones que cruzan múltiples bounded contexts usando eventos y compensaciones
- Outbox Pattern: Garantiza que los Domain Events se publiquen de forma confiable junto con los cambios de datos
- Anti-Corruption Layer (ACL): Implementa la relación de Context Mapping que protege tu dominio de modelos externos
- Shared Kernel: Implementa la relación de Context Mapping donde dos contextos comparten un subconjunto del modelo
- API Gateway / BFF: Proporcionan puntos de entrada que pueden mapear requests externos a los bounded contexts internos
Errores comunes al aplicar DDD
| Error | Consecuencia | Solución |
|---|---|---|
| Aplicar DDD a todo el sistema | Sobre-ingeniería en áreas simples | Usa DDD solo en el core domain, CRUD para el resto |
| Bounded contexts demasiado grandes | Monolito distribuido | Divide por subdominios reales, no por capas técnicas |
| Bounded contexts demasiado pequeños | Servicios parlanchines | Agrupa funcionalidades que cambian juntas |
| Ignorar el lenguaje ubicuo | Código desconectado del negocio | Sesiones regulares con expertos del dominio |
| Modelo anémico | Lógica dispersa en servicios | Mueve el comportamiento a las entidades |
| Agregados demasiado grandes | Problemas de concurrencia | Mantén agregados pequeños, usa eventos entre ellos |
Próximos pasos
Con DDD como base para modelar dominios complejos, el siguiente paso natural es entender cómo manejar transacciones que cruzan múltiples bounded contexts. Para eso existe el Saga Pattern, que coordina operaciones distribuidas con compensaciones. También explora el Microservices Pattern para entender cómo los bounded contexts se convierten en servicios independientes.