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:

  1. Organiza sesiones de Event Storming con expertos del dominio y desarrolladores
  2. Identifica los términos clave que usa el negocio y documéntalos en un glosario compartido
  3. Usa esos términos exactos en el código, los tests, la documentación y las conversaciones
  4. 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ónDescripciónEjemplo
Shared KernelDos contextos comparten un subconjunto del modeloTipos de moneda compartidos entre Ventas y Facturación
Customer-SupplierUn contexto provee datos que otro consumeInventario (supplier) provee stock a Pedidos (customer)
ConformistUn contexto se adapta al modelo de otro sin negociaciónTu sistema se adapta al modelo de la API de un proveedor externo
Anti-Corruption LayerUn contexto traduce el modelo externo a su propio lenguajeAdapter que traduce respuestas de un sistema legacy
Open Host ServiceUn contexto expone un protocolo abierto para que otros lo consumanAPI REST pública con documentación OpenAPI
Published LanguageSe 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íaOpciones
Frameworks DDDAxon Framework (Java), EventSourcing.NetCore (C#), Ecotone (PHP)
Event SourcingEventStoreDB, Axon Server, Marten (.NET)
CQRSMediatR (.NET), Axon (Java), custom implementations
MensajeríaApache Kafka, RabbitMQ, Amazon EventBridge
ModeladoEvent Storming, Domain Storytelling, Context Mapping
PersistenciaPostgreSQL, 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

ErrorConsecuenciaSolución
Aplicar DDD a todo el sistemaSobre-ingeniería en áreas simplesUsa DDD solo en el core domain, CRUD para el resto
Bounded contexts demasiado grandesMonolito distribuidoDivide por subdominios reales, no por capas técnicas
Bounded contexts demasiado pequeñosServicios parlanchinesAgrupa funcionalidades que cambian juntas
Ignorar el lenguaje ubicuoCódigo desconectado del negocioSesiones regulares con expertos del dominio
Modelo anémicoLógica dispersa en serviciosMueve el comportamiento a las entidades
Agregados demasiado grandesProblemas de concurrenciaManté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.