y este en el body:

Introducción

Este artículo demostrará la implementación de un Microservicio orientado a eventos con la ayuda del framework Axon. El tema está estrechamente relacionado con el artículo anterior (que se puede ver en este enlace), en el que explicamos los conceptos en los que se basa el framework.

Axon Framework: Elevating Microservices with DDD, CQRS, and Event Sourcing.

Axon Framework funciona como un poderoso aliado para los desarrolladores Java que navegan por las complejidades de la arquitectura de microservicios. Basado en tres pilares fundamentales: diseño orientado al dominio (DDD), segregación de responsabilidad de consulta de comandos (CQRS) y abastecimiento de eventos, Axon Framework permite a los desarrolladores crear sistemas escalables, resistentes y fáciles de mantener. En el ámbito de DDD, Axon alinea estrechamente los modelos de software con el dominio del problema, adoptando conceptos como agregados, entidades y objetos de valor. Con CQRS, divide a la perfección las responsabilidades de mando y consulta, optimizando las operaciones de forma independiente. Event Sourcing, otro principio básico, garantiza un historial cronológico de los cambios del sistema, ofreciendo transparencia y la capacidad de reconstruir los estados del sistema. Axon Framework es más que una herramienta; es un facilitador de una arquitectura cohesiva. Permite la construcción de microservicios basados en eventos que encarnan los principios de DDD, gestionan eficientemente comandos y consultas, y persisten eventos para una comprensión holística de la evolución del sistema. A medida que profundizamos en Axon, descubriremos sus características, componentes y aplicaciones prácticas, explorando cómo guía a los desarrolladores para liberar todo el potencial de DDD, CQRS y Event Sourcing en el desarrollo de microservicios.

Framework Axon: Una introducción

Visión General y Propósito

Axon Framework es como un conjunto de herramientas de superhéroes para desarrolladores Java que se sumergen en microservicios basados en eventos. No se trata solo de un conjunto de herramientas, sino de una guía que simplifica la creación de sistemas dinámicos y con capacidad de reacción.

  • Descripción general
    • En términos sencillos: Imagine Axon como un conjunto especial de engranajes y artilugios diseñados para crear aplicaciones que respondan a eventos: acciones, cambios o actualizaciones en su sistema.
    • Ejemplo: Es como tener un cinturón de herramientas de superhéroe equipado con herramientas para cada escenario de gestión de eventos.
  • Propósito:
    • Explicación: Axon existe para hacer la vida de los desarrolladores más fácil, proporcionando un enfoque estructurado para hacer frente a los eventos en un sistema.
    • Ejemplo: Es su guía de confianza, que le ayuda a navegar por las complejidades de la creación de software que reacciona sin problemas a diversos eventos.

Principales características y componentes

Axon Framework viene repleto de funciones y componentes interesantes que hacen que el proceso de desarrollo sea más sencillo y agradable:

  • Agregados:
    • Función: Son como equipos dedicados que manejan aspectos específicos de los datos de tu sistema.
    • Ejemplo: Imagine los agregados como mini-departamentos dentro de su empresa, cada uno responsable de gestionar su parte del negocio.
  • Manejadores de comandos y Manejadores de eventos:
    • Función: Imagínese a los gestores de comandos como administradores de tareas que garantizan la ejecución de los comandos. Los controladores de eventos son como informadores que toman nota de los cambios.
    • Ejemplo: Los gestores de comandos hacen las cosas, mientras que los gestores de eventos mantienen el sistema actualizado e informado.
  • Bus de comandos:
    • Función: Piensa en él como el gestor de tráfico de tu sistema, dirigiendo los comandos a los lugares adecuados.
    • Ejemplo: Es como un policía de tráfico que se asegura de que cada comando llegue a su destino sin caos.
  • Bus de eventos:
    • Función: Este es tu servicio de mensajería, entregando noticias (eventos) a diferentes rincones de tu aplicación.
    • Ejemplo: Como un mensajero que recorre la oficina, notificando los cambios a los departamentos pertinentes.

Beneficios de Axon Framework en Microservicios Dirigidos por Eventos:

Puede que te estés preguntando por qué Axon Framework es el héroe que necesitas para los microservicios orientados a eventos. Vamos a desglosar las ventajas utilizando ejemplos cotidianos:

  • Manejo simplificado de eventos: Axon le proporciona una forma organizada de manejar eventos, haciéndolo tan fácil como encontrar archivos en un armario bien estructurado. Es como tener un cajón etiquetado para cada evento, para que siempre sepa dónde buscar.
  • Modularidad y escalabilidad: Con Axon, puede construir como con ladrillos LEGO: unidades modulares que pueden crecer de forma independiente. Es como añadir más habitaciones a su casa sin tener que reconstruir toda la estructura.
  • Separación clara de preocupaciones: Axon le ayuda a mantener las cosas ordenadas animando a los departamentos de su aplicación a ocuparse de sus propios asuntos. Como los diferentes equipos en un evento deportivo – cada uno tiene su papel, y cada uno se centra en su parte sin confusión.

En pocas palabras, Axon Framework es su compañero de confianza, proporcionando herramientas prácticas y una filosofía de superhéroe para hacer frente a los retos de la construcción de microservicios orientados a eventos con facilidad y diversión.

Instalación de Axon Framework y configuración de dependencias

Antes de sumergirse en el código, debe asegurarse de que Axon Framework forma parte de su proyecto.

Usando Gradle:

Añade lo siguiente a tu build para un archivo project.gradle de Gradle:

dependencies {
    // Other dependencies
    implementation 'org.axonframework:axon-spring-boot-starter:4.5' // Replace with the latest version
}

Este código consigue el mismo resultado que la configuración de Maven, asegurando que Axon forma parte de su proyecto.

Inicializando un nuevo proyecto Axon con Spring Boot

Ahora que Axon está en su proyecto, vamos a inicializar un nuevo proyecto Axon con Spring Boot.

  • Crear un proyecto Spring Boot: Utiliza tu IDE preferido (como IntelliJ o Eclipse) o Spring Initializr para crear un nuevo proyecto Spring Boot.
  • Añada la configuración de Axon: Cree una clase anotada con @SpringBootApplication para arrancar su aplicación Spring Boot.
@SpringBootApplication
public class OrderManagementDomain {
  public static void main(String[] args) {
    SpringApplication.run(OrderManagementDomain.class, args);
  }
}

Configure las propiedades de Axon:

  • En su fichero application.properties o application.yml, configure las propiedades de Axon. He aquí un ejemplo mínimo:
axon.axonserver.servers=localhost:8124

# DB Read
spring.datasource.driver-class-name=org.postgresql.Driver
spring.datasource.url=jdbc:postgresql://localhost:5432/order_management
spring.datasource.username=order
spring.datasource.password=order123

spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
spring.jpa.hibernate.ddl-auto=create
#spring.jpa.properties.hibernate.format_sql=true
#spring.jpa.properties.hibernate.show_sql=true
spring.jpa.properties.hibernate.hbm2ddl.auto=updat
spring:
  datasource:
    driver-class-name: org.postgresql.Driver
    username: order
    url: jdbc:postgresql://localhost:5432/order_management
    password: order123
  jpa:
    properties:
      hibernate:
        dialect: org.hibernate.dialect.PostgreSQLDialect
        hbm2ddl:
          auto: update
    hibernate:
      ddl-auto: create
axon:
  axonserver:
    servers: localhost:8124

El archivo de propiedades que has proporcionado parece ser un archivo de configuración para una aplicación Spring Boot que utiliza Axon Framework con Axon Server para el aprovisionamiento de eventos y PostgreSQL como base de datos subyacente. Vamos a desglosar las propiedades:

Configuración de Axon Server:

axon:
  axonserver:
    servers: localhost:8124

Esta propiedad configura la conexión a Axon Server. Especifica que Axon Server se ejecuta localmente en el puerto 8124.

Configuración de la base de datos:

spring:
  datasource:
    driver-class-name: org.postgresql.Driver
    username: order
    url: jdbc:postgresql://localhost:5432/order_management
    password: order123

Estas propiedades configuran la conexión a una base de datos PostgreSQL.

spring.datasource.driver-class-name: Especifica la clase del controlador JDBC para PostgreSQL.

spring.datasource.url: Especifica la URL JDBC para conectarse a la base de datos PostgreSQL denominada «order_management» en localhost.

spring.datasource.username y spring.datasource.password: Proporcionan las credenciales para conectarse a la base de datos.

Configuración de Hibernate y JPA:

spring: 
 jpa:
    properties:
      hibernate:
        dialect: org.hibernate.dialect.PostgreSQLDialect
        hbm2ddl:
          auto: update
    hibernate:
      ddl-auto: create

spring.jpa.properties.hibernate.dialect Especifica el dialecto de Hibernate para PostgreSQL.

spring.jpa.hibernate.ddl-auto=create: Configura Hibernate para crear automáticamente el esquema de la base de datos. Tenga en cuenta que esto puede ser arriesgado en un entorno de producción, ya que puede provocar la pérdida de datos. Cámbielo a update para actualizaciones automáticas.

spring.jpa.properties.hibernate.hbm2ddl.auto=update: similar a ddl-auto, esta propiedad indica a Hibernate que actualice el esquema automáticamente.

Propiedades comentadas:

Las líneas que empiezan por # están comentadas. Puede descomentarlas si es necesario.

spring.jpa.properties.hibernate.format_sql=true: Si no se comenta, formatea las consultas SQL para una mejor legibilidad.

spring.jpa.properties.hibernate.show_sql=true: Si no se comenta, registra las consultas SQL ejecutadas.

En general, este archivo de configuración está configurando la aplicación para utilizar Axon Server para el manejo de eventos y PostgreSQL como la base de datos con Hibernate como el proveedor de JPA. Es importante tener en cuenta que la propiedad ddl-auto debe utilizarse con precaución, especialmente en entornos de producción. Asegúrate siempre de tener copias de seguridad adecuadas y considera el uso de herramientas de migración de bases de datos para cambios de esquema más controlados.

Infraestructura

La infraestructura utilizada en este artículo está basada en docker-compose, con Axon server Community Edition y una base de datos SQL (no por ninguna razón específica), como Postgres, para gestionar el modelo de lectura. Aquí está el archivo utilizado para dar un poco más de contexto.

version: '3.8'
services:
  postgres:
    image: postgres:latest
    container_name: postgres
    environment:
      POSTGRES_USER: order
      POSTGRES_PASSWORD: order123
      POSTGRES_DB: order_management
    ports:
      - "5432:5432"
    volumes:
      - postgres-data:/var/lib/postgresql/data

  # Axon Server
  axon-server:
    image: axoniq/axonserver:latest
    container_name: axon-server
    ports:
      - "8024:8024"
      - "8124:8124"
    depends_on:
      - postgres
    volumes:
      - axon-server-data:/data
volumes:
  postgres-data:
  axon-server-data:

  • version: Especifica la versión del formato de archivo Docker Compose.
  • services: Define los servicios que componen la aplicación.
    • postgres: Define el servicio PostgreSQL.
      • image: postgres:latest: Especifica la imagen Docker a utilizar para el contenedor PostgreSQL.
      • container_name: postgres: Asigna un nombre personalizado al contenedor PostgreSQL.
      • environment: Establece variables de entorno para PostgreSQL, incluyendo el usuario de la base de datos, la contraseña y el nombre de la base de datos.
      • ports: Mapea el puerto 5432 de la máquina anfitriona al puerto 5432 del contenedor para PostgreSQL.
      • volumes: Monta un volumen Docker llamado postgres-data para persistir datos PostgreSQL.
    • axon-server: Define el servicio Axon Server.
      • image: axoniq/axonserver:latest: Especifica la imagen Docker para Axon Server.
      • container_name: axon-server:Asigna un nombre personalizado al contenedor Axon Server.
      • ports: Mapea los puertos 8024 y 8124 de la máquina anfitriona a los puertos del contenedor para Axon Server.
      • depends_on: Especifica que el servicio Axon Server depende del servicio postgres (espera a que esté listo).
      • volumes: Monta un volumen Docker llamado axon-server-data para persistir los datos de Axon Server.
  • volumes: Define volúmenes Docker para la persistencia de datos.
    • postgres-data: Volumen para datos PostgreSQL.
    • axon-server-data: Volumen para datos de Axon Server.

Esta configuración establece un entorno de desarrollo con una base de datos PostgreSQL y Axon Server para una aplicación que utiliza Axon Framework.

Caso de uso seleccionado

La exploración de Axon Framework, DDD, CQRS y Event Sourcing dentro de un caso de uso de comercio electrónico proporciona un escaparate práctico de estos conceptos, ofreciendo información sobre el manejo de una lógica de negocio rica, interacciones en tiempo real y retos de escalabilidad. La naturaleza dinámica del dominio del comercio electrónico subraya los beneficios de la adaptabilidad y la integridad de los datos, haciendo que la exploración sea directamente aplicable y atractiva para la comprensión de estos patrones arquitectónicos. La característica que vamos a explorar será el carrito de la compra.

Definir el modelo de dominio con DDD: una guía práctica

En el intrincado panorama de los sistemas de comercio electrónico, un Diseño Orientado al Dominio (DDD) bien elaborado es la piedra angular para construir aplicaciones robustas, escalables y mantenibles. Profundicemos en los conceptos clave y ofrezcamos ejemplos concretos utilizando un caso de uso de comercio electrónico.

Identificación de contextos delimitados (Bounded-Contexts)

Los contextos delimitados, parte integral del Diseño Orientado al Dominio (DDD), establecen territorios distintos en los que modelos o términos específicos tienen significados únicos. En nuestro entorno de comercio electrónico, contextos delimitados como «Gestión de pedidos», «Control de inventario» y «Relaciones con los clientes» actúan como zonas encapsuladas, cada una con su propio dominio semántico.

// Bounded Context: Order Management
public class OrderItem {
    private Product product;
    private int quantity;
    // other fields and methods
}

// Bounded Context: Inventory Control
public class OrdeItem {
    private Product product;
    private int availableQuantity;
    // other fields and methods
}

Lista de Contextos Delimitados (Bounded Context)

Esta parte de contexto delimitado ilustra un proceso real; para cada contexto delimitado, tendrías uno o más microservicios, por ejemplo. Sigamos. En un sistema de comercio electrónico, los contextos delimitados pueden incluir:

  1. Gestión de pedidos: Gestión del ciclo de vida de los pedidos de los clientes, desde su creación hasta su cumplimiento.
  2. Gestión de inventario: Gestión de la disponibilidad y los niveles de existencias de los productos.
  3. Gestión de clientes: Gestión de las operaciones relacionadas con los clientes, como el registro, la autenticación y los perfiles.
  4. Procesamiento de pagos: Gestión de transacciones de pago y garantía de métodos de pago seguros.
  5. Catálogo de productos: Gestión de la información relacionada con los productos, como detalles, categorías y especificaciones.
  6. Envío y logística: Gestión del envío y la entrega de productos.
  7. Experiencia del usuario: Centrarse en el frontend y la interacción con el usuario, garantizando una experiencia de compra fluida e intuitiva.
  8. Marketing y promociones: Implementar estrategias de promoción de productos, descuentos y campañas de marketing.
  9. Atención al cliente: Responder a las consultas y quejas de los clientes y prestar servicios de asistencia.
  10. Análisis e informes: Recopilación y análisis de datos para obtener información sobre el comportamiento de los clientes, las tendencias de ventas y el rendimiento del sistema.

Cada contexto delimitado encapsula aspectos específicos del dominio general del comercio electrónico, lo que permite un modelado más claro, un desarrollo más sencillo y un mejor mantenimiento del sistema.

En el Contexto Delimitado de la Gestión de Pedidos dentro de un sistema de comercio electrónico, la selección de la Raíz Agregada es una decisión crucial que debe alinearse con las reglas y requisitos centrales del negocio. En este contexto, el candidato más adecuado para la Raíz Agregada suele ser la entidad «Pedido».

Justificación:

  1. Representación natural: Un «Pedido» representa de forma natural un concepto fundamental dentro del contexto de la Gestión de Pedidos. Encierra información esencial como detalles del cliente, selección de productos, cantidades y estado del pedido.
  2. Gestión del ciclo de vida: La entidad «Pedido» sigue un ciclo de vida, desde la creación hasta el cumplimiento y las posibles devoluciones. Esto se alinea con la necesidad de gestionar el ciclo de vida completo del pedido de un cliente.
  3. Coherencia y atomicidad: La agrupación de entidades relacionadas, como las partidas o los detalles del pedido, bajo el agregado «Pedido», garantiza la coherencia y la atomicidad. Los cambios en los pedidos y los componentes asociados pueden tratarse como una única transacción, lo que garantiza la integridad de los datos.
  4. Centralización de reglas de negocio: Las reglas de negocio relacionadas con la validación de pedidos, transiciones de estado y otras restricciones específicas de los pedidos pueden centralizarse en el agregado «Pedido», convirtiéndolo en un lugar natural para aplicar y gestionar estas reglas.
  5. Comunicación simplificada: El agregado «Pedido» proporciona un punto de entrada claro para la comunicación y la interacción dentro del contexto de la gestión de pedidos. Otras partes del sistema, como el inventario o el procesamiento de pagos, pueden comunicarse con el agregado «Pedido» para cumplir con sus responsabilidades.
  6. Escalabilidad: El agregado «Pedido» permite un escalado eficiente y un procesamiento paralelo de los pedidos. Los pedidos pueden gestionarse de forma independiente, reduciendo la contención y permitiendo el procesamiento paralelo para manejar un gran volumen de pedidos de forma concurrente.

Al elegir la entidad «Pedido» como raíz agregada para el contexto delimitado de gestión de pedidos, el sistema puede lograr una estructura cohesiva y bien organizada que se alinea con los procesos y requisitos empresariales básicos para gestionar los pedidos de los clientes.

// Aggregate: Order
public class OrderAggregate {
	private UUID orderId;
	private UUID customerId;
	private List<OrderItem> orderItems;
	private BigDecimal totalAmount;
	private DeliveryAddress deliveryAddress;
	private OrderStatus status;
  // other fields and methods
}

// Entity: Order Item
public class OrderItem {
  private UUID productId;
  private String productName;
  private BigDecimal price;
  private Integer quantity;
  // other fields and methods
}

// Value Object: DeliveryAddress
public class DeliveryAddress {
  private String street;
  private String city;
  private String state;
  private String zipCode;
  // other fields and methods
}

Agregación de raíces y relaciones

Definición:

  • Raíces agregadas: Son como las puertas principales a un grupo de cosas que queremos tratar juntas.
  • Relaciones: Son conexiones entre diferentes elementos de nuestro sistema.

Ejemplo: Considere «Order» como la puerta principal (raíz agregada). Nos lleva al «Client» y al «OrderItem». El «OrderItem» es como un puente entre «Order» y «Product». Así, cuando tratamos con pedidos, podemos encontrar y conectar fácilmente información relacionada como clientes y los artículos que pidieron. Ayuda a mantener todo organizado.

Ejemplo de código (Java):

// Aggregate: Order
public class OrderAggregate {
	private UUID orderId;
	private UUID customerId;
	private List<OrderItem> orderItems;
	private BigDecimal totalAmount;
	private DeliveryAddress deliveryAddress;
	private OrderStatus status;
  // other fields and methods
}

// Entity: Order Item
public class OrderItem {
  private UUID productId;
  private String productName;
  private BigDecimal price;
  private Integer quantity;
  // other fields and methods
}

Ejemplo de código con Axon framework

@Aggregate
public class OrderAggregate {
	
	@AggregateIdentifier
	private UUID orderId;
	private UUID customerId;
	@AggregateMember
	private List<OrderItem> orderItems;
	private BigDecimal totalAmount;
	private DeliveryAddress deliveryAddress;
	private OrderStatus status;
  1. @Aggregate: Esta anotación marca la clase OrderAggregate como un agregado de Axon. En Axon Framework, un agregado es un objeto de dominio responsable de manejar órdenes, aplicar eventos y gestionar la consistencia de su estado.
  2. @AggregateIdentifier: Esta anotación marca un campo (orderId en este caso) como identificador del agregado. El identificador es crucial para que Axon identifique y rastree instancias del agregado de forma única.
  3. @AggregateMember: Esta anotación se aplica al campo List<OrderItem> orderItems. Indica que OrderItem forma parte del agregado, y cuando el OrderAggregate persiste, las instancias OrderItem relacionadas se almacenarán como parte del agregado.

En resumen, estas anotaciones ayudan a Axon a entender la estructura y el comportamiento de la clase OrderAggregate en el contexto de la generación de eventos y el manejo de comandos dentro de Axon Framework. El @AggregateIdentifier especifica el identificador único, y @AggregateMember indica la asociación con otra clase dentro del agregado.

Comandos y Eventos en DDD

Definición:

  • Piensa en Comandos como peticiones para hacer cambios en el sistema. Son como instrucciones de lo que debe ocurrir. Una vez creados, los comandos están grabados en piedra, lo que significa que son inalterables o «inmutables».
  • Los Eventos son registros permanentes de eventos importantes del sistema. Capturan momentos dignos de mención y son inmutables, al igual que los comandos.

Ejemplo: Imagine un «ComandoCrearOrden» como una instrucción específica para crear una orden. Cuando este comando se ejecuta, crea un «OrderCreatedEvent», un registro duradero que contiene información crucial sobre la creación exitosa del pedido.

Ejemplo de código de comando (Java):

// Command: Create Order
public record CreateOrderCommand(UUID orderId,
                                 UUID customerId,
                                 List<OrderItem> orderItems,
                                 DeliveryAddress deliveryAddress,
                                 OrderStatus status)  {
                                 // other fields and methods
}

Mientras recorremos estos conceptos DDD, nuestro caso de uso de comercio electrónico cobrará vida, mostrando la aplicación práctica de estos principios en la construcción de un sistema resistente y bien estructurado.

Ejemplo de código utilizando Axon Framework

public record CreateOrderCommand(@TargetAggregateIdentifier UUID orderId,
                                 UUID customerId,
                                 List<OrderItem> orderItems,
                                 DeliveryAddress deliveryAddress,
                                 OrderStatus status)  {}
  1. @TargetAggregateIdentifier: Esta anotación es específica de Axon Framework. Indica que el campo anotado (UUID orderId en este caso) es el identificador de la raíz del agregado. En el contexto de Axon, una raíz de agregado es un tipo específico de entidad que es el punto de entrada principal a un agregado. En este registro, le dice a Axon que orderId es el identificador del pedido.
  2. record: Se trata de una característica introducida en Java 16 para crear clases de datos inmutables. Los registros generan automáticamente métodos como equals(), hashCode() y toString(). Son concisos y proporcionan una forma sencilla de modelar datos.

Así, en términos más simples, la anotación @TargetAggregateIdentifier ayuda a Axon a identificar qué campo sirve como identificador del agregado, y la palabra clave record significa que se trata de una clase de datos concisa e inmutable para un CreateOrderCommand.

Ejemplo de código de evento:

// Event: Order Placed
public record OrderCreatedEvent(UUID orderId,
                                UUID customerId,
                                List<OrderItem> orderItems,
                                DeliveryAddress deliveryAddress,
                                OrderStatus status) {
                               // other fields and methods
}

No se requiere ninguna anotación del Axon Framework para el evento.

Hasta ahora, hemos visto que los datos están estructurados. Ahora veremos dónde y cómo se comunican.

Cómo enviar comandos

En nuestro proyecto de ejemplo, el punto de entrada del Sistema/Microservicio es una API Rest. Aquí hay un ejemplo de cómo enviar un comando al framework Axon, cómo se envía un comando para ser procesado, y cómo se deben manejar los resultados positivos y negativos:

@PostMapping(value = "/",consumes = "application/json")
	public CompletableFuture<ResponseEntity<String>> createOrder(@RequestBody CreateOrderRequest order) {
		// Add logic to create the order
		logger.info("CreateOrderRequest request: {}", order);
		CreateOrderCommand command = CreateOrderCommand.from(order);
		logger.info("CreateOrderCommand request: {}", command);
		
		return this.commandGateway.send(command)
			.thenApply(result -> {
				logger.info("OrderAggregate processing started successfully.");
				return ResponseEntity.ok("OrderAggregate processing started successfully.");
			})
			.exceptionally(exception -> {
				logger.error("Exception occurred during command execution", exception);
				return ResponseEntity.badRequest().body(exception.getMessage());
			});
	}
  1. CreateOrderCommand command = CreateOrderCommand.from(order): Crea un CreateOrderCommand a partir de la CreateOrderRequest entrante. Este comando representa probablemente la intención de crear un pedido.
  2. return this.commandGateway.send(command)...: Envía el CreateOrderCommand a la pasarela de comandos de Axon para su procesamiento. Las funciones thenApply y exceptionally forman parte de la API CompletableFuture:
    • .thenApply(result -> {...}): Se ejecuta cuando el procesamiento del comando tiene éxito. Registra un mensaje de éxito y devuelve una ResponseEntity con un estado OK y un mensaje de éxito.
    • .exceptionally(exception -> {...}): Se ejecuta cuando se produce una excepción durante el procesamiento del comando. Registra un mensaje de error y devuelve una ResponseEntity con un estado de solicitud errónea y un mensaje de error.

En términos más sencillos, este código representa la gestión de una solicitud POST para crear un pedido. Registra la solicitud entrante, la convierte en una orden, la envía para su procesamiento de forma asíncrona, y luego maneja los escenarios de éxito o fracaso con los mensajes de registro y respuesta apropiados.

Cómo manejar un comando

Una vez que el comando ha sido enviado al commandBus, para procesarlo, debes crear un método en la raíz del agregado donde será procesado. También hay que tomar decisiones, por ejemplo, si crear un tipo de evento u otro en función del estado de la raíz del Agregado.

@CommandHandler
	public OrderAggregate(CreateOrderCommand command) {
		Preconditions.checkArgument(command != null, "CreateOrderCommand cannot be null");
		Preconditions.checkArgument(command.orderId() != null, "orderId cannot be null in the CreateOrderCommand");
		AggregateLifecycle.apply(OrderCreatedEvent.from(command));
	}
  1. @CommandHandler: Esta anotación marca este método como manejador de un comando. En este caso, maneja comandos de tipo **CreateOrderCommand**.
  2. Preconditions.checkArgument(command != null, "CreateOrderCommand cannot be null"): Esta línea comprueba si el **CreateOrderCommand** entrante no es nulo. Si es nulo, lanza una IllegalArgumentException con el mensaje de error especificado.
  3. Preconditions.checkArgument(command.orderId() != null, "orderId cannot be null in the CreateOrderCommand"): Esta línea comprueba si la propiedad orderId del CreateOrderCommand no es nula. Si es nula, lanza una IllegalArgumentException con el mensaje de error especificado.

Creo que sería importante comentar aquí lo que no queda claro en muchos artículos y otras fuentes: cuando se procesa un Command, hay que validar la información que se está recibiendo, por ejemplo, el estado del Aggregate raíz, para ver si se puede procesar el evento, en este caso con la ayuda del framework ya se ha cargado el Aggregate, pero en otros casos lo primero sería recuperar el estado actual del Aggregate raíz, para tener los datos con los que trabajar.

Cómo aplicar eventos

Cuando los datos hayan sido validados, y el estado de la raíz agregada sea correcto, se disparará un evento, momento en el que se producirá el cambio de estado. Veamos esto en detalle, con el mismo ejemplo:

@CommandHandler
	public OrderAggregate(CreateOrderCommand command) {
		Preconditions.checkArgument(command != null, "CreateOrderCommand cannot be null");
		Preconditions.checkArgument(command.orderId() != null, "orderId cannot be null in the CreateOrderCommand");
		AggregateLifecycle.apply(OrderCreatedEvent.from(command));
	}

AggregateLifecycle.apply(OrderCreatedEvent.from(command)): Si el comando pasa las comprobaciones de precondición, esta línea aplica un evento al ciclo de vida del agregado. Llama al método apply en AggregateLifecycle y proporciona una instancia de OrderCreatedEvent creada a partir del CreateOrderCommand dado. Este evento probablemente representa el hecho de que se está creando un nuevo pedido.

Cómo manejar un Evento

Hay un punto clave aquí, y es que cuando disparas el evento usando AggregateLifecycle.apply, el evento será enviado primero a todos los manejadores dentro de la Raíz del Agregado, ninguna otra Raíz del Agregado recibirá el evento y después de procesar el evento dentro del agregado, será enviado al Bus de Eventos y será procesado transaccionalmente.

Para manejar el evento dentro de la Raíz del Agregado, debería verse así:

@EventSourcingHandler
	public void handle(OrderCreatedEvent event) {
		this.orderId = event.orderId();
		this.customerId = event.customerId();
		this.orderItems = event.orderItems();
		this.totalAmount = calculateTotalAmount(event.orderItems());
		this.deliveryAddress = event.deliveryAddress();
		this.status = event.status();
	}
  1. @EventSourcingHandler: Esta anotación forma parte de Axon Framework. Marca el método como un manejador de eventos. En el contexto de Axon y Event Sourcing, especifica que este método debe manejar eventos mientras se reconstruye el estado de un agregado.
  2. public void handle(OrderCreatedEvent event): Este método está diseñado para manejar eventos de tipo OrderCreatedEvent. Se disparará cuando se produzca un evento de este tipo, y su propósito es actualizar el estado del agregado correspondiente (OrderAggregate en este caso) en base a la información proporcionada en el evento.

En resumen, la anotación @EventSourcingHandler marca un método que actualiza el estado de un agregado (OrderAggregate) en función de los eventos que recibe durante el proceso de aprovisionamiento de eventos. El método maneja el OrderCreatedEvent extrayendo la información relevante y actualizando el estado del agregado en consecuencia.

Ahora, para manejar eventos después de que han sido manejados dentro del Agregado, debes hacer lo siguiente:

@EventHandler
	public void on(OrderCreatedEvent event) {
        logger.info("Projecting OrderCreatedEvent: {}", event);
		// Add logic to project the OrderCreatedEvent
		OrderEntity orderEntity = new OrderEntity(event);
		orderEntityRepository.save(orderEntity);
		
    }
  1. @EventHandler: Esta anotación forma parte de Axon Framework. Marca el método como manejador de eventos. En el contexto de Axon, especifica que este método debe manejar eventos. A diferencia de @EventSourcingHandler, que se utiliza durante la reconstrucción del estado de un agregado, @EventHandler se utiliza para proyectar eventos para actualizar modelos de lectura o sistemas externos.
  2. public void on(OrderCreatedEvent event): Este método está diseñado para manejar eventos de tipo OrderCreatedEvent. Se disparará cuando se produzca un evento de este tipo.

En resumen, la anotación @EventHandler marca un método que maneja eventos (OrderCreatedEvent en este caso).

Proyecciones

Como último paso en el procesamiento de Comandos y Eventos, en CQRS donde se procesa la escritura en este enfoque, existe la Proyección, que es la transformación/mapeo entre los modelos de escritura y lectura.

Continuando con el código anterior, o mejor dicho implementando el método que crea una entidad que será almacenada en la base de datos con el modelo de lectura basado en los datos del evento que fue manejado.

Entidad modelo de lectura

@Entity
@Table(name = "orders")
public class OrderEntity {
	
	@Id
	@Column(name = "order_id")
	private UUID orderId;
	@Column(name = "customer_id")
	private UUID customerId;
	@OneToMany(mappedBy = "orderEntities", cascade = CascadeType.MERGE, orphanRemoval = true)
	private List<OrderItemEntity> orderItems;
	@Column(name = "total_amount")
	private BigDecimal totalAmount;
	@Embedded
	private DeliveryAddress deliveryAddress;
	@Enumerated(EnumType.STRING)
	@Column(name = "status")
	private OrderStatus status;

	...
public OrderEntity(OrderCreatedEvent event) {
		this.orderId = event.orderId();
		this.customerId = event.customerId();
		this.orderItems = convertOrderItems(event.orderId(), event.orderItems());
		this.totalAmount = calculateTotalAmount(event.orderItems());
		this.deliveryAddress = event.deliveryAddress();
		this.status = event.status();
	}
...
}

Manejador de eventos con proyección

@EventHandler
	public void on(OrderCreatedEvent event) {
        logger.info("Projecting OrderCreatedEvent: {}", event);
		// Add logic to project the OrderCreatedEvent
		OrderEntity orderEntity = new OrderEntity(event);
		orderEntityRepository.save(orderEntity);
		
    }
  1. Creación de OrderEntity:proyection
    • OrderEntity parece ser una entidad de datos o un objeto de dominio que representa el pedido en el modelo de lectura. Se instancia con el OrderCreatedEvent como parámetro. Esto implica que el evento se utiliza para construir el estado de la OrderEntity.
  2. Almacenar en el repositorio:
    • orderEntityRepository.save(orderEntity); sugiere que la OrderEntity se guarda o actualiza en un repositorio. Este repositorio suele ser un almacén de datos (como una base de datos) donde se mantiene la vista optimizada para lectura (proyección).

Manejadores de consultas y modelos de lectura

En Axon Framework, un Manejador de Consultas es un componente responsable de manejar las consultas en una arquitectura CQRS (Segregación de Responsabilidad de Consulta de Comandos). Mientras que los Manejadores de Comandos manejan comandos (que representan acciones que cambian el estado del sistema), los Manejadores de Consultas manejan consultas, que son solicitudes de información u operaciones de lectura que no cambian el estado del sistema.

Como se presenta en la proyección, el modelo de lectura facilita la recuperación de datos para presentarlos al solicitante, ya sea un frontend u otro servicio.

Definición de consultas

Para utilizar el framework Axon, se deben definir clases POJO con los parámetros de la Query a ejecutar.

  • En una arquitectura CQRS, las consultas son objetos que representan peticiones de información. Se diferencian de los comandos, que representan peticiones para cambiar el estado del sistema.
  • Las consultas suelen incluir parámetros que definen el contexto de la información solicitada.
public record FindOrderByIdQuery(UUID uuid) {}
public record FindAllOrdersQuery() {}

Manejadores de consultas en Axon

  • Un manejador de consultas es una clase o un método responsable de manejar un tipo específico de consulta.
  • Los manejadores de consultas en Axon están anotados con @QueryHandler para indicar que manejan consultas.
  • El método handler debe tener una firma que coincida con la consulta que se supone que debe manejar.
@Service
public class OrderQueryHandler {
	...
	
    @QueryHandler
    public OrderDTO findById(FindOrderByIdQuery query) {
		Optional<OrderEntity> orderEntity = orderEntityRepository.findById(query.uuid());
		if (orderEntity.isPresent()) {
			logger.info("Order found: {}", orderEntity.get());
			return OrderDTO.from(orderEntity.get());
		}else {
			logger.info("Order not found: {}", query.uuid());
			throw new OrderNotFoundException("Order not found: " + query.uuid());
		}
    }
	
	@QueryHandler
	public OrderQueryListAllResponse findAll(FindAllOrdersQuery query) {
		List<OrderEntity> orderEntities = orderEntityRepository.findAll();
		List<OrderDTO> result = convertOrderEntities(orderEntities);
		logger.info("Order found: {}", result);
        return new OrderQueryListAllResponse(result);
    }
...
	
}

Anotación @Service:

  • La anotación @Service se utiliza para indicar que esta clase es un componente de servicio de Spring. Forma parte de Spring Framework y se utiliza a menudo para definir un service bean.

Método Query Handler:

  • El método findById está anotado con @QueryHandler, indicando que maneja consultas de tipo FindOrderByIdQuery.
  • Se asume que FindOrderByIdQuery es una clase de consulta que representa una petición para encontrar un pedido por su ID.

Lógica de manejo de consultas:

  • El método recupera el parámetro FindOrderByIdQuery, que probablemente contiene el ID del pedido a recuperar.
  • A continuación, utiliza orderEntityRepository.findById(query.uuid()) para intentar encontrar la OrderEntity correspondiente en un almacén de datos. Se supone que orderEntityRepository es un repositorio u objeto de acceso a datos para OrderEntity.

Cómo ejecutar consultas

  • Las consultas se envían a los gestores de consultas a través del bus de consultas de Axon.
  • El bus de consultas es responsable de enrutar las consultas a sus correspondientes gestores.
  • Desacopla el remitente de la consulta del gestor real, lo que permite un diseño más modular y escalable.
@GetMapping(value = "/")
	public CompletableFuture<ResponseEntity<?>> getAllOrders() {
		FindAllOrdersQuery query = new FindAllOrdersQuery();
		logger.info("GetOrderByIdQuery request: {}", query);
		CompletableFuture<OrderQueryListAllResponse> result = queryGateway.query(query, OrderQueryListAllResponse.class);
		logger.info("Result: {}", result);
		return result
			.thenApply(orderList -> {
				if (orderList.orders().isEmpty()) {
					logger.info("No orders found");
					return ResponseEntity.notFound().build();
				} else {
					logger.info("Orders found: {}", orderList);
					return ResponseEntity.ok(orderList.orders());
				}
			})
			.exceptionally(exception -> {
				logger.error("Exception occurred during query execution", exception);
				return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
			});
	}

Creación de la consulta:

  • Se crea una instancia de FindAllOrdersQuery. Esta consulta probablemente representa una solicitud para recuperar todos los pedidos.

Ejecución de la consulta:

  • La consulta se ejecuta de forma asíncrona utilizando la pasarela de consultas de Axon (queryGateway.query(query, OrderQueryListAllResponse.class)).
  • El resultado es un CompletableFuture<OrderQueryListAllResponse>.

Manejo del resultado con thenApply:

  • El método thenApply maneja el resultado cuando la ejecución de la consulta finaliza con éxito.
  • Si la lista de pedidos está vacía (orderList.orders().isEmpty()), se devuelve una respuesta no encontrada (ResponseEntity.notFound().build()).
  • Si se encuentran los pedidos, se devuelve una respuesta correcta (ResponseEntity.ok(orderList.orders())) con la lista de pedidos.

Manejo de excepciones con exceptionally:

  • El método exceptionally maneja las excepciones que puedan ocurrir durante la operación asíncrona.

Si se produce una excepción, se registra un mensaje de error y se devuelve una respuesta de error del servidor (ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build()).

Conclusión

En esta segunda parte de la exploración de CQRS y Event Sourcing con Axon Framework, hemos profundizado en sus principios fundacionales y aspectos prácticos de implementación. Centrándonos en la utilización del potente Axon Framework para operaciones de Comandos, Eventos y Consultas, navegamos por el intrincado mundo del diseño de microservicios escalables, mantenibles y orientados a eventos.

Primera parte: Comprender los fundamentos

Comenzamos desentrañando los conceptos clave del Diseño Orientado al Dominio (DDD), la Segregación de Responsabilidades de Consulta de Comandos (CQRS) y el Abastecimiento de Eventos. Gracias a un conocimiento exhaustivo de los contextos limitados, los agregados, las entidades y los objetos de valor, sentamos las bases para construir sistemas sólidos y conscientes del contexto.

Link: https://www.enmilocalfunciona.io/unveiling-the-power-of-event-driven-microservices-a-comprehensive-guide-to-ddd-cqrs-and-event-sourcing-with-axon-framework/

Parte 2: Poner Axon Framework en acción

Partiendo de los conocimientos básicos, adoptamos un enfoque práctico en la Parte 2 explorando el papel de Axon Framework en la implementación de microservicios basados en eventos. Navegamos a través de escenarios del mundo real, desde la configuración del entorno de desarrollo hasta la definición del modelo de dominio utilizando DDD, y desde la implementación de CQRS con Axon hasta el manejo de eventuales desafíos de consistencia.

Aprovechar la potencia de Axon Framework

Axon Framework demostró ser algo más que una herramienta; se convirtió en nuestro aliado en la creación de sistemas escalables, resistentes y poco acoplados. A través de sus anotaciones, como @Aggregate, @EventSourcingHandler y @EventHandler, fuimos testigos de cómo Axon integra a la perfección DDD, CQRS y Event Sourcing en nuestro flujo de trabajo de desarrollo.

Al concluir este viaje, recuerde que el verdadero poder de Axon Framework no reside sólo en su sintaxis o anotaciones, sino en su capacidad para dar forma a nuestro pensamiento, guiar nuestras decisiones de diseño, y darnos el poder de construir sistemas que evolucionan con elegancia con las demandas siempre cambiantes del panorama digital.

Por lo tanto, tanto si se está embarcando en un nuevo proyecto de microservicios como si está mejorando uno existente, considere Axon Framework como su compañero en el reino de CQRS y Event Sourcing.

Que tus microservicios florezcan, tus eventos se propaguen sin problemas y tus consultas sean rápidas como el rayo.

El proyecto de ejemplo está en un repositorio git en GitHub en el siguiente enlace:

Si desea más información sobre cómo trabajar con eventos, en nuestro blog encontrará una serie de artículos sobre el tema elaborados por otro colega: