Domain-Driven Design (DDD)
Cómo DDD propone modelar el problema de negocio: Strategic Design (subdomains, bounded contexts, ubiquitous language, context maps) y Tactical Design (entities, value objects, aggregates, domain events y repositories).
Mientras que los Drivers de Arquitectura responden:
¿Qué necesita el sistema y qué lo condiciona?
Domain-Driven Design responde:
¿Cómo entendemos y modelamos correctamente el problema de negocio?
DDD propone centrar el diseño en el domain: sus conceptos, sus reglas y su lenguaje.
flowchart TD B["Business"] --> D["Domain"] D --> C1["Concepts"] D --> C2["Rules"] D --> C3["Processes"] D --> C4["Relationships"]
DDD tiene dos grandes dimensiones: Strategic Design y Tactical Design.
Strategic Design
El Strategic Design intenta responder:
- ¿qué partes del negocio existen?
- ¿cómo se relacionan?
- ¿dónde están los boundaries?
- ¿qué lenguaje se utiliza?
- ¿qué partes son estratégicamente importantes?
Domain y Subdomains
Imaginemos una plataforma de e-commerce:
flowchart TD E["E-commerce"] --> Cat["Catalog"] E --> Ord["Orders"] E --> Pay["Payments"] E --> Inv["Inventory"] E --> Ship["Shipping"] E --> Cus["Customers"]
Cada una de estas áreas puede representar un subdomain diferente, y no todas tienen necesariamente la misma importancia.
Core Domain
Es la parte donde existe una ventaja o diferenciación importante. Payments sería Core si la plataforma tiene capacidades de payment processing que representan una ventaja competitiva.
Supporting Domain
Necesario para el negocio, pero sin valor diferencial. Un ejemplo típico es Inventory.
Generic Domain
Funcionalidad común, como Authentication.
Esta clasificación ayuda a decidir dónde invertir en diseño propio y dónde apoyarse en soluciones existentes.
Bounded Context
Es uno de los conceptos más importantes de DDD. Un Bounded Context define un límite dentro del cual un modelo y su lenguaje tienen un significado específico.
flowchart TD E["E-commerce"] --> P["Payments"] E --> O["Orders"] E --> S["Shipping"] P --> PC["Customer = payer"] O --> OC["Customer = buyer"] S --> SC["Customer = recipient"]
El término Customer puede existir en los tres contextos sin representar el mismo modelo en cada uno.
Bounded Context ≠ Microservicio
Esta distinción es fundamental:
| Concepto | Qué representa |
|---|---|
| Bounded Context | Límite conceptual y de dominio |
| Microservicio | Límite de deployment / runtime |
Un Bounded Context puede implementarse como un módulo, un boundary dentro de un monolith o un microservicio. Por eso DDD no implica microservicios.
Ubiquitous Language
El equipo y los domain experts deberían compartir un lenguaje. En Payments:
Authorize
Capture
Refund
Payment
Transaction
Provider
Settlement
La terminología debería aparecer también en el software:
payment.authorize()
payment.capture()
payment.refund()
y no esconder conceptos importantes detrás de nombres genéricos como payment.process(), payment.update() o payment.handle() cuando esos métodos abarcan múltiples reglas de negocio.
Context Map
Los bounded contexts no existen aislados. Podemos representar sus relaciones:
flowchart LR Pay["Payments"] -- publishes --> Ord["Orders"] Ord -- consumes --> Inv["Inventory"]
DDD dispone de patrones para describir estas relaciones, como Customer/Supplier, Conformist, Anti-Corruption Layer, Shared Kernel, Open Host Service y Published Language.
Un Anti-Corruption Layer, por ejemplo, protege el modelo interno de un contexto frente a conceptos provenientes de otro sistema:
flowchart LR Ext["External Provider Model"] --> ACL["Anti-Corruption Layer"] --> Our["Our Domain Model"]
Es particularmente útil al integrar sistemas legacy o terceros con modelos poco alineados.
Tactical Design
Strategic DDD define los boundaries. Tactical DDD trabaja sobre el modelo interno, con building blocks como:
- Entity
- Value Object
- Aggregate
- Aggregate Root
- Domain Service
- Domain Event
- Repository
- Factory
Entity
Tiene identidad persistente:
Payment
PaymentId = 123
Dos Payments pueden tener los mismos valores, pero seguir siendo entidades diferentes si tienen identidades diferentes.
Value Object
Se define por sus valores:
Money(100, ARS)
Currency(ARS)
Email(...)
Address(...)
Un Money(100, ARS) no necesita un MoneyId.
Aggregate
Un Aggregate es una frontera dentro de la cual se protege un conjunto de invariantes.
flowchart TD A["Payment Aggregate"] --> P["Payment (root)"] A --> PT["PaymentTransaction"] A --> PM["PaymentMethod"]
El Aggregate Root es el punto de entrada:
payment.authorize()
payment.capture()
payment.refund()
No queremos que cualquier código pueda modificar arbitrariamente payment.status = CAPTURED, porque podría saltarse reglas del dominio.
Proteger esas invariantes dentro del aggregate es un problema de diseño con solución conocida: cuando lo que varía según el estado son las operaciones permitidas —un pedido enviado ya no se puede cancelar—, el patrón State evita que esa regla quede repartida en condicionales por todo el código.
Aggregate Design
Una idea importante es no construir Aggregates gigantes. Un Aggregate debería contener aquello que necesita mantener consistencia transaccional. Podría ser tentador modelar todo como un único Aggregate:
flowchart TD O["Order"] --> I["100 OrderItems"] O --> P["Payments"] O --> C["Customer"] O --> S["Shipping"] O --> N["Inventory"]
Pero eso produciría transacciones muy grandes, lock contention, alto acoplamiento y menor escalabilidad. DDD ayuda precisamente a preguntar:
¿Qué cosas realmente necesitan cambiar juntas?
Domain Service
Existe lógica de dominio que no pertenece naturalmente a una sola Entity. Por ejemplo, un FraudDecisionService que combina varias fuentes:
flowchart LR P["Payment"] --> F["FraudDecisionService"] C["Customer"] --> F T["TransactionHistory"] --> F R["RiskPolicy"] --> F
No significa que todos los servicios de una aplicación deban ser Domain Services. Un PaymentService con veinte responsabilidades suele ser una señal de que el modelo de dominio no expresa las reglas con suficiente claridad.
Domain Events
Un Domain Event expresa un hecho —PaymentAuthorized, PaymentCaptured, PaymentRefunded— y no una orden como CapturePayment.
flowchart LR C["Payment.capture()"] --> E["PaymentCaptured"] E --> O["Orders"] E --> N["Notifications"] E --> A["Analytics"]
Los Domain Events conectan DDD con los patrones de comunicación event-driven, pero no implican por sí mismos que necesitemos Kafka o microservicios.
Repository
Un Repository abstrae la persistencia de un Aggregate. PaymentRepository podría implementarse como PostgresPaymentRepository: el dominio depende de la abstracción, no de PostgreSQL. Esta idea conecta directamente con Hexagonal, Onion y Clean Architecture.
Factory
Una Factory puede encapsular una creación compleja, como Payment.create(...), si para crear un Payment hay que validar múltiples condiciones o garantizar invariantes. Para objetos triviales como new Money(100, ARS), introducir una Factory puede ser innecesario.
Ejemplo integrado
flowchart TD BC["Payments Bounded Context"] --> Dom["Domain"] BC --> App["Application"] Dom --> P["Payment"] Dom --> PID["PaymentId"] Dom --> M["Money"] Dom --> PT["PaymentTransaction"] Dom --> Repo["PaymentRepository"] Dom --> DE["Domain Events"] DE --> DE1["PaymentAuthorized"] DE --> DE2["PaymentCaptured"] App --> A1["AuthorizePayment"] App --> A2["CapturePayment"] App --> A3["RefundPayment"]
Cuándo DDD es especialmente útil
- dominios complejos;
- muchas reglas de negocio;
- terminología ambigua;
- equipos grandes;
- múltiples subdomains;
- sistemas que evolucionarán durante años.
Cuándo no hace falta usar todo DDD
Para un CRUD muy simple (Users, Products, Categories), crear Aggregates, Domain Services, Factories y Context Maps para cada operación puede introducir más ceremonia que valor. DDD es sobre todo una herramienta para lidiar con la complejidad del dominio, no una checklist.
Herramientas y tecnologías
DDD no requiere una tecnología específica. Puede implementarse con Java + Spring, C# + .NET, Kotlin, TypeScript, Go o Python. Lo importante es el modelo, no el framework.
Referencias
- Eric Evans sentó las bases de Domain-Driven Design en Domain-Driven Design: Tackling Complexity in the Heart of Software.
- Microsoft Azure Architecture Center presenta cómo utilizar bounded contexts y principios de Domain-Driven Design para definir límites de microservicios.
