Domina la Arquitectura Hexagonal y DDD para Microservicios

En el mundo moderno del desarrollo de software, la arquitectura de microservicios se ha convertido en un estándar debido a sus ventajas en escalabilidad y mantenimiento. Un microservicio es una unidad pequeña y autónoma de funcionalidad que interactúa con otros servicios a través de APIs bien definidas. En esta guía, vamos a explorar la configuración de un microservicio con múltiples integraciones. Este ejemplo incluye servicios esenciales como Apache Kafka, MongoDB, ActiveMQ Artemis y HTTP

Además, este microservicio sigue el patrón de arquitectura hexagonal con el enfoque de diseño dirigido por dominio (DDD). Esto significa que la lógica de negocio se encuentra en el centro del diseño, y las dependencias externas están en los bordes, permitiendo una mejor modularización y prueba unitaria de la lógica de negocio.

Introducción a la Arquitectura Hexagonal

La arquitectura hexagonal, también conocida como arquitectura de puertos y adaptadores, fue introducida por Alistair Cockburn con el objetivo de crear un diseño de software que sea más fácil de mantener y extender. Esta arquitectura busca separar la lógica de negocio del sistema de las dependencias externas, como bases de datos, interfaces de usuario y otros servicios.


src/main/java/com/kranio/
  ├── application
  │   ├── mappers
  │   │   └── UserMapper.java
  │   ├── services
  │   │   └── UserService.java
  ├── domain
  │   ├── classes
  │   │   └── User.java
  │   ├── repositories
  │   │   └── IUserRepository.java
  ├── infrastructure
  │   ├── amq/repositories
  │   │   └── AMQProducerRepository.java
  │   ├── mongodb/repositories
  │   │   └── MongoRepository.java
  ├── presenters
  │   ├── http
  │   │   └── UserController.java
  │   ├── kafka
  │   │   └── KafkaConsumer.java
 

Explicación de los Componentes

1. Application

• mappers

• UserMapper.java: Clase para mapear datos entre diferentes capas de la aplicación.



package com.kranio.application.mappers;
	
	import jakarta.enterprise.context.ApplicationScoped;
	import com.kranio.domain.classes.User;
	import jakarta.xml.bind.JAXBContext;
	import jakarta.xml.bind.JAXBException;
	import jakarta.xml.bind.Unmarshaller;
	
	import java.io.StringReader;
	
	@ApplicationScoped
	public class UserMapper {
	
	  public User convertStringToUser(String xml) throws JAXBException {
	    JAXBContext jaxbContext = JAXBContext.newInstance(User.class);
	    Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
	    StringReader reader = new StringReader(xml);
	    return (User) unmarshaller.unmarshal(reader);
	  }
	}



• services

• UserService.java: Implementación de la lógica de negocio del servicio de usuario.



package com.kranio.application.services;
	
	import jakarta.enterprise.context.ApplicationScoped;
	import jakarta.inject.Inject;
	import com.kranio.domain.repositories.IUserRepository;
	import com.kranio.application.mappers.UserMapper;
	import com.kranio.domain.classes.User;
	
	@ApplicationScoped
	public class UserService {
	
	  @Inject
	  private IUserRepository userRepository;
	
	  @Inject
	  private UserMapper userMapper;
	
	  public void saveAndSendMessage(String message) {
	    try {
	      User user = userMapper.convertStringToUser(message);
	      String response = userRepository.save(user);
	      userRepository.sendMessage(response);
	    } catch (Exception e) {
	      System.err.println("Error saving User: " + e.getMessage());
	    }
	  }
	
	  public void saveAndSendMessage(User user) {
	    try {
	      String response = userRepository.save(user);
	      userRepository.sendMessage(response);
	    } catch (Exception e) {
	      System.err.println("Error saving User: " + e.getMessage());
	    }
	  }
	}
  



2. Domain

• classes

• User.java: Clase que representa el modelo de usuario.



package com.kranio.domain.classes;
	
	import io.quarkus.mongodb.panache.PanacheMongoEntity;
	import io.quarkus.mongodb.panache.PanacheMongoEntityBase;
	import lombok.Data;
	
	public class User extends PanacheMongoEntity {
	    public String name;
	    public String email;
	}



• repositories

• IUserRepository.java: Interfaz que define las operaciones de persistencia de usuario.



package com.kranio.domain.repositories;
	
	import com.kranio.domain.classes.User;
	
	public interface IUserRepository {
	  public void sendMessage(String message);
	
	  public String save(User user);
	}
  



3. Infrastructure

• amq/repositories

• AMQProducerRepository.java: Implementación del puerto de salida para interactuar con ActiveMQ.



package com.kranio.infrastructure.amq.repositories;
	
	import jakarta.enterprise.context.ApplicationScoped;
	import jakarta.inject.Inject;
	import jakarta.jms.ConnectionFactory;
	import jakarta.jms.JMSContext;
	import jakarta.jms.JMSRuntimeException;
	import jakarta.jms.Session;
	import com.kranio.domain.classes.User;
	import com.kranio.domain.repositories.IUserRepository;
	import com.kranio.infrastructure.mongodb.repositories.MongoRepository;
	
	@ApplicationScoped
	public class AMQProducerRepository implements IUserRepository {
	
	  @Inject
	  private ConnectionFactory connectionFactory;
	
	  @Inject
	  private MongoRepository mongoRepository;
	
	  public void sendMessage(String message) {
	    try (JMSContext context = connectionFactory.createContext(Session.AUTO_ACKNOWLEDGE)) {
	      context.createProducer().send(context.createQueue("TestQueue"), message);
	      System.out.println("Message sent: " + message);
	    } catch (JMSRuntimeException ex) {
	      System.err.println("Error sending message: " + ex.getMessage());
	    }
	  }
	
	  public String save(User user) {
	    mongoRepository.persist(user);
	    return "User saved";
	  }
	}




• mongodb/repositories

• MongoRepository.java: Implementación del puerto de salida para interactuar con MongoDB.



package com.kranio.infrastructure.mongodb.repositories;
	
	import io.quarkus.mongodb.panache.PanacheMongoRepository;
	
	import jakarta.enterprise.context.ApplicationScoped;
	import com.kranio.domain.classes.User;
	
	@ApplicationScoped
	public class MongoRepository implements PanacheMongoRepository *User* {
	}



4. Presenters

• http

• UserController.java: Controlador que maneja las solicitudes HTTP y las convierte en llamadas a la lógica de negocio a través del servicio de usuario.



package com.kranio.presenters.http;
	
	import jakarta.inject.Inject;
	import jakarta.ws.rs.POST;
	import jakarta.ws.rs.Path;
	import jakarta.ws.rs.core.Response;
	
	import com.kranio.application.services.UserService;
	import com.kranio.domain.classes.User;
	
	@Path("/user")
	public class UserController {
	
	  @Inject
	  private UserService userService;
	
	  @POST
	  public Response create(User newUser) {
	    userService.saveAndSendMessage(newUser);
	    return Response.status(Response.Status.CREATED).entity(newUser).build();
	  }
	}




• kafka

• KafkaConsumer.java: Consumidor de mensajes de Kafka que interactúa con la lógica de negocio.



package com.kranio.presenters.kafka;
	
	import org.eclipse.microprofile.reactive.messaging.Incoming;
	import io.vertx.mutiny.ext.auth.User;
	import jakarta.inject.Inject;
	import jakarta.inject.Singleton;
	import com.kranio.application.services.UserService;
	
	@Singleton
	public class KafkaConsumer {
	
	  @Inject
	  private UserService userService;
	
	  @Incoming("consumer")
	  public void consume(String message) {
	    System.out.println("Consumiendo mensaje: " + message);
	    userService.saveAndSendMessage(message);
	  }
	}




Esta estructura de proyecto basada en la arquitectura hexagonal permite una clara separación de responsabilidades, donde el núcleo de la aplicación (dominio) permanece independiente de las tecnologías y frameworks utilizados en los adaptadores. Esto facilita el mantenimiento, la escalabilidad y las pruebas unitarias, asegurando que la lógica de negocio se mantenga limpia y fácilmente adaptable a futuros cambios.

Beneficios y Debilidades de la Arquitectura Hexagonal

Beneficios

1. Separación de Concerns: Facilita el mantenimiento al separar la lógica de negocio de las dependencias técnicas.

2. Flexibilidad y Extensibilidad: Permite agregar nuevas funcionalidades o cambiar tecnologías sin afectar la lógica central.

3. Pruebas Unitarias: Facilita la creación de pruebas unitarias al permitir el uso de mocks o stubs para las dependencias externas.

4. Independencia Tecnológica: Hace más sencillo cambiar de tecnologías (bases de datos, sistemas de mensajería, etc.) sin modificar la lógica de negocio.

Debilidades

1. Complejidad Inicial: Requiere una mayor planificación y definición de puertos y adaptadores desde el inicio.

2. Sobrecarga de Abstracción: Puede introducir una sobrecarga innecesaria de abstracción en proyectos pequeños.

3. Curva de Aprendizaje: Puede ser más difícil de entender y aplicar correctamente para desarrolladores sin experiencia en esta arquitectura.

4. Rendimiento: La capa adicional de abstracción puede introducir una ligera disminución en el rendimiento, aunque generalmente es insignificante comparado con los beneficios.

Diseño Dirigido por el Dominio (DDD)

El Diseño Dirigido por el Dominio (DDD, por sus siglas en inglés) es un enfoque de desarrollo de software que pone un fuerte énfasis en el modelado del dominio de negocio central del software. Introducido por Eric Evans en su libro “Domain-Driven Design: Tackling Complexity in the Heart of Software”, DDD ofrece un conjunto de principios y patrones para crear sistemas de software que reflejan profundamente el dominio de negocio y las necesidades de los usuarios. 

Conceptos Clave de DDD

1. Entidades: Objetos que tienen una identidad distintiva que atraviesa el tiempo y diferentes estados. Por ejemplo, un usuario con un ID único.

2. Objetos de Valor: Objetos que se definen completamente por sus atributos. No tienen identidad propia. Por ejemplo, una dirección.

3. Agregados: Un grupo de entidades y objetos de valor que se tratan como una unidad. Tienen una entidad raíz que controla el acceso.

4. Repositorios: Facilitan el acceso a los agregados. Actúan como una colección en memoria de los agregados.

5. Servicios de Dominio: Operaciones que no pertenecen a ninguna entidad o objeto de valor en particular pero son parte del dominio.

6. Módulos: Agrupan conceptos relacionados para organizar el código.

7. Fábricas: Encargadas de la creación de objetos complejos o agregados.

  

Beneficios de DDD

1. Modelo de Negocio Claro: DDD ayuda a crear un modelo de negocio claro y compartido entre los desarrolladores y los expertos en el dominio.

2. Código Más Comprensible: El código se vuelve más comprensible porque refleja directamente los términos y procesos del dominio del negocio.

3. Reducción de Complejidad: Al centrarse en el núcleo del dominio y sus reglas, DDD ayuda a gestionar la complejidad inherente a los sistemas de software.

4. Mejora de la Comunicación: Mejora la comunicación entre desarrolladores y expertos en el dominio mediante un lenguaje ubicuo (Ubiquitous Language) común.

5. Flexibilidad y Mantenibilidad: La separación clara de responsabilidades y la modularización facilitan la extensión y el mantenimiento del sistema.

Debilidades de DDD

1. Curva de Aprendizaje Pronunciada: Requiere una buena comprensión de los conceptos de DDD y puede ser difícil de adoptar para los equipos sin experiencia.

2. Sobrecarga Inicial: El proceso de modelado puede ser intensivo y llevar más tiempo en las primeras fases del desarrollo.

3. Complejidad en Proyectos Pequeños: Puede introducir una complejidad innecesaria en proyectos pequeños o simples.

4. Necesidad de Colaboración Constante: Requiere una colaboración continua entre desarrolladores y expertos en el dominio, lo cual puede ser un desafío en algunos contextos.

5. Dificultad en la Implementación: La correcta identificación de los límites del contexto y la división del dominio puede ser desafiante y puede requerir varias iteraciones.

Y asi se ve el microservicio que estará adjunto para que le des una mirada, lo clones y juegues un poco. lee el README.md para ver como se ejecuta y todas las consideraciones a tener en cuenta. 

https://github.com/eljoesb/ddd-kranio-blog

Jonathan Villavicencio

August 9, 2024