Arquitectura DDD y Hexagonal: Construyendo Software para el Futuro
- Mauricio ECR
- Arquitectura
- 05 Jul, 2025
En el dinámico mundo del desarrollo de software, la complejidad es el enemigo silencioso. Las aplicaciones crecen, los requisitos cambian y, sin una guía clara, el código puede convertirse rápidamente en un laberinto frágil y costoso de mantener. ¿La solución? No es un framework de moda, sino una filosofía de diseño sólida. Esta guía ofrece un mapa detallado para construir software robusto, escalable y, sobre todo, alineado con el negocio, fusionando los principios del Diseño Guiado por el Dominio (DDD), la Arquitectura Limpia (Clean Architecture) y la Arquitectura Hexagonal (Puertos y Adaptadores).
Olvídate de las capas anémicas y el acoplamiento tecnológico. Aquí aprenderás a colocar el corazón de tu negocio —el dominio— en el centro del universo, protegido y aislado de los detalles mundanos de la tecnología. Prepárate para diseñar sistemas donde la lógica de negocio es la reina, la infraestructura es un sirviente intercambiable y el cambio es una oportunidad, no una amenaza.
🎯 Capa de Dominio: El Corazón del Negocio
Esta es la capa más sagrada y protegida de la arquitectura. Su único propósito es encapsular la lógica y las reglas de negocio puras, utilizando el Lenguaje Ubicuo (Ubiquitous Language) del problema que se está resolviendo. Es completamente agnóstica a la tecnología; no debe existir ninguna referencia a frameworks, bases de datos o APIs. En Arquitectura Hexagonal, esta capa es el “hexágono” central, y en Arquitectura Limpia, corresponde a los círculos internos de Entidades y Casos de Uso.
Aquí residen los componentes que modelan el negocio: Agregados, Entidades, Objetos de Valor, Eventos de Dominio, Servicios de Dominio, las interfaces de los Repositorios (que actúan como Puertos) y los Casos de Uso que orquestan toda la lógica.
| Componente | Rol y Responsabilidad Clave |
|---|---|
| Agregado (Aggregate) | Unidad de consistencia transaccional. Agrupa entidades y objetos de valor bajo una raíz (Aggregate Root) que protege las reglas de negocio del clúster. |
| Entidad (Entity) | Objeto con una identidad única que perdura en el tiempo y un ciclo de vida definido. Su identidad es lo que lo define, no sus atributos. |
| Objeto de Valor (VO) | Objeto inmutable definido por sus atributos, sin una identidad propia. Se utiliza para medir, cuantificar o describir cosas (ej. Dinero, FechaRango). |
| Evento de Dominio | Representa un suceso de negocio relevante que ya ha ocurrido. Sirve para comunicar cambios y desacoplar la lógica entre diferentes partes del sistema. |
| Servicio de Dominio | Encapsula lógica de negocio sin estado que no pertenece de forma natural a ninguna entidad u objeto de valor, a menudo coordinando varios de ellos. |
| Repositorio (Puerto) | Define el contrato para persistir y recuperar agregados. Es una interfaz que dicta las necesidades del dominio sin conocer la tecnología subyacente. |
| Caso de Uso | Orquesta el flujo de una operación. Es el punto de entrada a la lógica de dominio, recibiendo datos de entrada y utilizando los puertos para ejecutar la acción. |
🔌 Capa de Infraestructura: El Mundo de la Tecnología
Esta capa contiene todos los detalles técnicos y las implementaciones concretas. Su finalidad es servir como un conjunto de adaptadores que traducen las interacciones del mundo exterior al lenguaje del dominio, y viceversa. Implementa los puertos definidos en la Capa de Dominio, cumpliendo con la sagrada Regla de la Dependencia: la infraestructura siempre depende del dominio. Aquí residen los frameworks web, las conexiones a bases de datos, los clientes de servicios externos y cualquier otra dependencia del mundo real.
Los componentes clave son los Entry-Points (adaptadores que invocan los casos de uso, como controladores de API REST) y los Driven-Adapters (implementaciones de los puertos del dominio, como un repositorio JPA), junto con sus artefactos de apoyo como DTOs, Modelos de Persistencia y Mappers.
| Componente | Rol y Responsabilidad Clave |
|---|---|
| Entry-Point | Adaptador que recibe una señal externa (ej. una petición HTTP, un mensaje de una cola) y la traduce en una llamada a un Caso de Uso. |
| DTO (Data Transfer Object) | Define la estructura de datos para la comunicación externa. Se usa en los Entry-Points para modelar peticiones y respuestas, aislando el dominio. |
| Driven-Adapter | Implementación de un puerto del dominio. Por ejemplo, un repositorio que usa JPA para hablar con una base de datos o un cliente HTTP para consumir otra API. |
| Modelo de Persistencia | Clase que mapea a una estructura de base de datos (ej. una tabla). Es un detalle de implementación del adaptador de persistencia, no es la entidad de dominio. |
| Mapper / Traductor | Utilidad para convertir datos entre capas: DTO ↔ Entidad, Modelo de Persistencia ↔ Entidad. Es el pegamento que permite el desacoplamiento. |
🚀 Capa de Aplicación: El Ensamblador
Esta es la capa más externa y conceptualmente simple. Su única finalidad es ensamblar la aplicación y ponerla en marcha. No contiene lógica de negocio. Es responsable de inicializar el sistema, configurar el contenedor de Inyección de Dependencias (IoC) para conectar las implementaciones de la infraestructura (Driven-Adapters) con las abstracciones del dominio (Puertos), y leer configuraciones externas.
| Componente | Rol y Responsabilidad Clave |
|---|---|
| Contenedor IoC | Configuración de la Inyección de Dependencias. Define “recetas” para construir los objetos, especificando qué Driven-Adapter se debe usar para un Puerto. |
| Configuración | Gestiona los parámetros externos de la aplicación (URLs, credenciales, etc.) a través de archivos (.yml, .properties) o variables de entorno. |
| Punto de Entrada | La clase que contiene el método public static void main(String[] args). Su única función es arrancar el framework y, con él, toda la aplicación. |
📂 Estructura de Módulos Sugerida
Una representación visual de una estructura de módulos (por ejemplo, en Gradle o Maven) que materializa esta arquitectura de forma limpia.
mi-proyecto-escalable/
├── build.gradle.kts
├── settings.gradle.kts # Define los módulos del proyecto
│
├── applications/
│ └── app-service/ # Capa de Aplicación: Ensambla y corre la app
│ └── src/main/java/com/miempresa/app/MainApplication.java
│ └── build.gradle.kts # Depende de 'domain' e 'infrastructure'
│
├── domain/ # Capa de Dominio: Lógica de negocio pura
│ ├── model/ # El modelo: agregados, entidades, VOs, eventos...
│ │ └── src/main/java/com/miempresa/domain/model/producto/Producto.java
│ │ └── src/main/java/com/miempresa/domain/model/producto/gateways/ProductoRepository.java # Puerto (Interfaz)
│ │ └── build.gradle.kts # No tiene dependencias de otras capas
│ └── usecase/ # Los casos de uso que orquestan el modelo
│ └── src/main/java/com/miempresa/domain/usecase/producto/ListarProductosUseCase.java
│ └── build.gradle.kts # Depende de 'domain/model'
│
└── infrastructure/ # Capa de Infraestructura: Detalles tecnológicos
├── entry-points/ # Adaptadores de entrada (ej. API REST)
│ └── rest-api/
│ └── src/main/java/com/miempresa/infrastructure/entrypoints/producto/ProductoController.java
│ └── build.gradle.kts # Depende de 'domain/usecase'
│
└── driven-adapters/ # Adaptadores de salida (ej. Repositorio JPA)
└── jpa-repository/
│ └── src/main/java/com/miempresa/infrastructure/drivenadapters/producto/ProductoData.java # Entidad JPA
│ └── src/main/java/com/miempresa/infrastructure/drivenadapters/producto/ProductoRepositoryAdapter.java # Adaptador
│ └── build.gradle.kts # Depende de 'domain/model'
🗺️ Diagrama de Flujo y Dependencias
Este diagrama ilustra la Regla de la Dependencia (flechas sólidas de dependencia ->) y el Flujo de Control (flechas punteadas de ejecución ...> ). Observa cómo las dependencias siempre apuntan hacia el interior, hacia el dominio, mientras que el flujo de control atraviesa las capas.
+-------------------------------------------------------------------------------------------------+
| Capa de Aplicación (Ensamblador, main) |
+-------------------------------------------------------------------------------------------------+
|
| Inicia y configura
V
+-------------------------------------------------------------------------------------------------+
| Capa de Infraestructura (Adaptadores: REST, DB, etc.) <-- Las flechas de DEPENDENCIA apuntan aquí |
| |
| +---------------+ FLUJO DE CONTROL ...> +--------------------+ |
| | Entry-Point | | Driven-Adapter | |
| | (Controller) | ... ... ... ... ... ... ... ... | (JPA Repository) | |
| +---------------+ . +--------------------+ |
| | ^ . ^ | |
| (Llama) | (Retorna DTO) . (Implementa) | (Habla con DB) |
| | . . . . . | V |
| V | +---------------+ |
| <DEPENDENCIA> <DEPENDENCIA> | Mundo Externo | |
+------+------------------------------------------------------+----------+---------------+-------------+
| |
V V
+-------------------------------------------------------------------------------------------------+
| Capa de Dominio (Lógica de Negocio Pura) <-- TODAS las DEPENDENCIAS apuntan aquí |
| |
| +---------------+ FLUJO DE CONTROL ...> +--------------------+ |
| | Caso de Uso | | Repositorio (Port) | |
| | (Orquestador) | ... ... ... ... ... ... ... ... | (Interfaz) | |
| +---------------+ . +--------------------+ |
| ^ | . |
| | V . |
| | +----------+ |
| +-> | Agregado | <... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...|
| +----------+ |
+-------------------------------------------------------------------------------------------------+
💡 Ejemplo Avanzado: POST /orders (Crear un Pedido)
Veamos cómo fluyen las interacciones en un proceso de negocio real y complejo, como la creación de un nuevo pedido.
1. La Petición del Cliente (Capa de Infraestructura)
- Componente:
OrderController(Entry-Point). - Acción: Recibe una petición
POSTen/orders. Su rol es validar el formato de la petición (usando unPlaceOrderRequestDTO) y delegar inmediatamente al caso de uso correspondiente. No sabe cómo se procesa un pedido, solo a quién llamar. - Artefacto:
PlaceOrderRequestDTO(DTO). Modela el JSON de entrada (customerId,items, etc.). Usa validaciones de framework (@NotNull,@Size) para un rechazo temprano.
2. La Orquestación Central (Capa de Dominio)
- Componente:
PlaceOrderUseCase(Caso de Uso). - Acción: Este es el director de orquesta. No contiene lógica de negocio en sí mismo, pero coordina los pasos en el orden correcto. Su constructor recibe, mediante inyección de dependencias, varios puertos (interfaces):
CustomerRepository,ProductRepository,OrderPricingService,PaymentGateway, yOrderRepository.
3. Recolección y Validación de Negocio (Dominio interactuando con Infraestructura)
- Paso 3.1: Validar Cliente: El caso de uso invoca
customerRepository.findById(customerId). ElCustomerRepositoryAdapter(en infraestructura) lo buscará en la base de datos. Si no existe, el dominio lanza una excepción de negocio (CustomerNotFoundException). - Paso 3.2: Validar Productos y Stock: Para cada ítem, invoca
productRepository.findById(productId). El AgregadoProductrecuperado es responsable de validar sus propias reglas, comoproduct.hasSufficientStock(quantity).
4. Ejecución de Lógica de Negocio Compleja (Dominio)
- Componente:
OrderPricingService(Servicio de Dominio). - Acción: El caso de uso le pasa el cliente y los productos. Este servicio, cuya lógica no encaja en un único agregado, calcula el precio total, aplicando descuentos por lealtad o promociones. Devuelve un Objeto de Valor
Money.
5. Creación del Nuevo Agregado (Dominio)
- Componente:
Order(Agregado Raíz). - Acción: Con todos los datos validados y el precio calculado, el caso de uso invoca un método de fábrica estático:
Order.create(customer, items, totalPrice). El agregadoOrderse crea en un estado inicial válido y registra un evento,OrderPlacedEvent.
6. Interacción con Servicios Externos (Infraestructura)
- Componente:
PaymentGateway(Puertoen el dominio) yStripePaymentAdapter(Driven-Adapteren infraestructura). - Acción: El caso de uso llama a
paymentGateway.processPayment(...). El dominio solo conoce la interfaz. La infraestructura proporciona la implementación concreta (StripePaymentAdapter) que se comunica con la API de Stripe. Si el pago falla, se lanza una excepción que aborta el caso de uso.
7. Persistencia y Efectos Secundarios (Dominio y Infraestructura)
- Paso 7.1: Guardar el Pedido: Si el pago es exitoso, el caso de uso llama a
orderRepository.save(order). ElOrderRepositoryAdapter, usando unOrderDataMapperpara convertir el agregado a unOrderData(entidad JPA), persiste el pedido en la base de datos de forma transaccional. - Paso 7.2: Publicar Evento de Dominio: Tras guardar exitosamente, el caso de uso (o un decorador del repositorio) invoca a un
DomainEventPublisher. Esto despacha elOrderPlacedEventregistrado previamente. Otros módulos del sistema, comoNotificationsoInventory, pueden escuchar este evento y reaccionar de forma totalmente desacoplada (enviar un email, actualizar el stock).
8. La Respuesta Final (Infraestructura)
- Componente:
OrderController(de nuevo). - Acción: Recibe el resultado exitoso del caso de uso (el agregado
Orderrecién creado), lo mapea a unPlaceOrderResponseDTOy devuelve una respuesta201 Createdcon el ID del nuevo pedido.
🏁 Conclusión: Más Allá del Código
Adoptar una arquitectura basada en DDD y Hexagonal no es simplemente organizar carpetas; es un cambio de mentalidad. Nos obliga a dialogar con los expertos del negocio, a modelar la complejidad del mundo real y a proteger esa lógica invaluable de los detalles efímeros de la tecnología.
Los beneficios clave son innegables:
- Testabilidad Superior: La lógica de negocio pura en el dominio puede ser probada unitariamente sin necesidad de frameworks, bases de datos o servidores web.
- Mantenibilidad y Evolución: Cambiar de una base de datos PostgreSQL a MongoDB, o de una API REST a gRPC, se convierte en la tarea de escribir un nuevo adaptador, sin tocar el núcleo del negocio.
- Enfoque en el Negocio: El equipo se centra en resolver problemas de negocio reales, ya que el código refleja directamente el lenguaje y los procesos de la empresa.
- Escalabilidad Organizacional: Diferentes equipos pueden trabajar en distintos adaptadores o módulos del dominio de forma paralela con un bajo riesgo de conflictos.
Esta arquitectura sienta las bases para patrones aún más avanzados como CQRS (Command Query Responsibility Segregation) y Event Sourcing, permitiendo que tus sistemas no solo respondan a las necesidades actuales, sino que estén preparados para prosperar ante los desafíos del futuro.