Empezamos la quinta entrega de esta serie de post de patrones de arquitectura. Por si os habéis perdido alguno, os dejo a continuación los artículos anteriores:

Hoy, seguimos hablando de comunicación y coordinación entre microservicios, y nos centraremos en Event Sourcing y arquitectura orientada a eventos (EDA). Más adelante, cerramos esta con CQRS (Command Query Responsibility Segregation), BFF (Backend for Frontend) y Outbox.

No terminaremos ahí, ya que queda mucho contenido para siguientes publicaciones donde veremos muchos más patrones y un ejemplo de escalabilidad y gestión de recursos con escalado automático y cómo pasar de monolitos a microservicios, configuración externalizada y Consumer-Driven Contract Testing.

Event Sourcing

El patrón Event Sourcing es una técnica de diseño de software que consiste en almacenar un registro secuencial de todos los cambios que ocurren en el estado de una aplicación como una secuencia de eventos inmutables. Cada evento representa un cambio único y atómico en el estado de la aplicación y se almacena de manera secuencial en un registro de eventos, generalmente llamado "log de eventos" o "journal". Este enfoque permite reconstruir el estado actual de la aplicación en cualquier punto en el tiempo mediante la reproducción de todos los eventos desde el inicio hasta el momento deseado.

Características de Event Sourcing

  • Registro de eventos inmutables: todos los eventos que representan cambios en el estado de la aplicación son inmutables y se almacenan de manera secuencial en un registro de eventos.
  • Reconstrucción del estado: el estado actual de la aplicación se reconstruye a partir de la secuencia de eventos aplicando cada evento en orden cronológico.
  • Historial completo de cambios: el registro de eventos proporciona un historial completo de todos los cambios que han ocurrido en la aplicación a lo largo del tiempo.
  • Escalabilidad y tolerancia a fallos: el patrón Event Sourcing facilita la escalabilidad y la tolerancia a fallos, ya que permite reproducir y reconstruir el estado de la aplicación en cualquier momento.

Ventajas de Event Sourcing

  • Auditoría y seguimiento: proporciona un registro completo de todas las acciones y cambios realizados en la aplicación, lo que facilita la auditoría y el seguimiento.
  • Reproducibilidad: permite reproducir el estado de la aplicación en cualquier momento y facilita la resolución de problemas y la depuración.
  • Flexibilidad y evolución: permite agregar nuevas funcionalidades retrospectivamente y adaptarse a cambios en los requisitos del negocio mediante la introducción de nuevos tipos de eventos.
  • Consistencia y atomicidad: los eventos son atómicos y se aplican de manera consistente, lo que garantiza la integridad del estado de la aplicación.

Desafíos del patrón Event Sourcing

  • Complejidad de implementación: la implementación de Event Sourcing puede ser más compleja que otros enfoques tradicionales debido a la necesidad de gestionar el registro de eventos y la reconstrucción del estado de la aplicación.
  • Escalabilidad del registro de eventos: el tamaño del registro de eventos puede crecer considerablemente con el tiempo, lo que puede plantear desafíos en términos de almacenamiento y rendimiento.
  • Consistencia eventual: la reconstrucción del estado de la aplicación a partir de eventos puede llevar tiempo, lo que puede resultar en una consistencia eventual en lugar de una consistencia inmediata.
  • Modelado de eventos: requiere un diseño cuidadoso del modelo de eventos para representar de manera precisa todos los cambios relevantes en el estado de la aplicación.

En resumen, el patrón Event Sourcing es una técnica poderosa para gestionar el estado de una aplicación de manera eficiente y escalable. Si se implementa correctamente, puede proporcionar una mayor transparencia, flexibilidad y capacidad de evolución en comparación con otros enfoques más tradicionales. Sin embargo, también presenta desafíos en términos de complejidad de implementación y escalabilidad del registro de eventos.

Ejemplo

Esto sería un evento base que representa cualquier acción que ocurra en nuestro sistema:

import java.time.LocalDateTime;

public abstract class Event {
    private final LocalDateTime timestamp;

    public Event() {
        this.timestamp = LocalDateTime.now();
    }

    public LocalDateTime getTimestamp() {
        return timestamp;
    }
}

Eventos específicos para acciones como:

  • La realización de una compra: cada vez que un cliente realiza una compra, se genera un evento de compra que contiene toda la información relevante sobre la transacción, como el ID de la compra, los productos comprados, el monto total, la información del cliente, etc.
public class PurchaseEvent extends Event {
    private final String purchaseId;
    // Otros campos relevantes
    
    public PurchaseEvent(String purchaseId) {
        this.purchaseId = purchaseId;
        // Inicializar otros campos
    }
    
    // Getters para campos relevantes
}
  • El procesamiento de un pago: cuando se procesa un pago para una compra, se genera un evento de pago que contiene detalles sobre la transacción de pago, como el ID de la compra, el método de pago utilizado, el monto pagado, etc.
public class PaymentEvent extends Event {
    private final String paymentId;
    // Otros campos relevantes
        
    public PaymentEvent(String paymentId) {
        this.paymentId = paymentId;
        // Inicializar otros campos
    }
        
    // Getters para campos relevantes
}
  • El envío de productos: una vez que se envían los productos comprados, se genera un evento de envío que registra la información de envío, como el ID de la compra, los productos enviados, la dirección de envío, etc.
public class ShippingEvent extends Event {
    private final String shipmentId;
    // Otros campos relevantes
            
    public ShippingEvent(String shipmentId) {
        this.shipmentId = shipmentId;
        // Inicializar otros campos
    }
            
    // Getters para campos relevantes
}

Luego, creamos un registro de eventos donde almacenaremos todos los eventos que ocurren en el sistema (en un caso real se deben utilizar técnicas de persistencia adecuadas para loguear los datos, colas, brokers de mensajería, etc.):

import java.util.ArrayList;
import java.util.List;

public class EventLog {
    private final List<Event> events;

    public EventLog() {
        this.events = new ArrayList<>();
    }

    public void addEvent(Event event) {
        events.add(event);
    }

    public List<Event> getEvents() {
        return events;
    }
}

Y, finalmente, en nuestro servicio de aplicación, podemos registrar eventos cada vez que ocurra una acción relevante en el sistema:

Para la realización de la compra:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class PurchaseService {
    private final EventLog eventLog;

    @Autowired
    public PurchaseService(EventLog eventLog) {
        this.eventLog = eventLog;
    }

    public void makePurchase(String purchaseId) {
        // Lógica para realizar la compra
        
        // Registrar el evento de compra
        PurchaseEvent purchaseEvent = new PurchaseEvent(purchaseId);
        eventLog.addEvent(purchaseEvent);
    }
}

Para la realización del pago:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;


@Service
public class PaymentService {
    private final EventLog eventLog;

    @Autowired
    public PaymentService(EventLog eventLog) {
        this.eventLog = eventLog;
    }

    public void processPayment(String paymentId) {
        // Lógica para procesar el pago
        
        // Registrar el evento de pago
        PaymentEvent paymentEvent = new PaymentEvent(paymentId);
        eventLog.addEvent(paymentEvent);
    }
}

Y para el envío:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;


@Service
public class ShippingService {
    private final EventLog eventLog;

    @Autowired
    public ShippingService(EventLog eventLog) {
        this.eventLog = eventLog;
    }

    public void shipProducts(String shipmentId) {
        // Lógica para enviar los productos
        
        // Registrar el evento de envío
        ShippingEvent shippingEvent = new ShippingEvent(shipmentId);
        eventLog.addEvent(shippingEvent);
    }
}

Ya vemos que de manera muy sencilla se puede montar un sistema de event sourcing simple. Esto nos puede servir para una trazabilidad de los pedidos, pudiendo ver, por ejemplo, cuáles se han completado correctamente o en cuáles ha habido algún problema mediante un servicio de informes o dashboard.

Event sourcing

EDA

La arquitectura orientada a eventos es un estilo arquitectónico que se centra en la generación, detección, procesamiento y respuesta a eventos que ocurren dentro de un sistema o entre sistemas distribuidos. En EDA, los sistemas están diseñados para ser altamente reactivos a los eventos, lo que permite una comunicación asincrónica y desacoplada entre los componentes del sistema.

Características de la arquitectura orientada a eventos

  • Eventos: los eventos son mensajes que representan una acción o una notificación que ocurre dentro del sistema. Pueden ser generados por usuarios, aplicaciones, dispositivos o cualquier otro origen y pueden ser consumidos por uno o más componentes para desencadenar acciones específicas.
  • Comunicación asincrónica: en EDA, la comunicación entre componentes se realiza de manera asincrónica a través del intercambio de eventos. Esto permite que los componentes se comuniquen de manera eficiente y desacoplada, lo que facilita la escalabilidad y la flexibilidad del sistema.
  • Desacoplamiento: los componentes en un sistema orientado a eventos están desacoplados entre sí, lo que significa que pueden funcionar de forma independiente y no están directamente acoplados a otros componentes.

Ventajas de la arquitectura orientada a eventos

  • Flexibilidad: EDA permite una mayor flexibilidad en el diseño y la evolución del sistema, ya que los componentes pueden ser modificados o reemplazados sin afectar a otros componentes.
  • Escalabilidad: la comunicación asincrónica y el desacoplamiento entre componentes mejoran la escalabilidad del sistema, ya que los componentes pueden escalar independientemente según sea necesario.
  • Reactividad: ayuda a que los sistemas sean altamente reactivos a los cambios y eventos que ocurren en el entorno, lo que facilita la implementación de sistemas en tiempo real y de alta disponibilidad.
  • Detección de patrones: EDA permite de un modo más sencillo la detección de patrones y la toma de decisiones basadas en eventos, lo que permite que los sistemas sean más inteligentes y adaptativos.

Desafíos de la arquitectura orientada a eventos

  • Complejidad de diseño: requiere un diseño cuidadoso del sistema y de los eventos para garantizar que los componentes se comuniquen de manera efectiva y que los eventos se procesen correctamente.
  • Gestión de eventos: la gestión de eventos puede ser compleja, especialmente en sistemas distribuidos con grandes volúmenes de eventos. Se requiere una infraestructura robusta para gestionar el intercambio, el procesamiento y el almacenamiento de eventos de manera eficiente.
  • Consistencia y coherencia: la comunicación asincrónica puede introducir desafíos en términos de mantener la consistencia y la coherencia de los datos entre los componentes del sistema.
  • Depuración y monitoreo: el monitoreo y la depuración de sistemas orientados a eventos pueden ser más complejos debido a la naturaleza asincrónica de la comunicación y la gran cantidad de eventos generados y procesados por el sistema.

En resumen, la arquitectura orientada a eventos es un enfoque arquitectónico poderoso que se centra en la generación, detección, procesamiento y respuesta a eventos dentro de un sistema o entre sistemas distribuidos. Ofrece numerosas ventajas, como desacoplamiento, escalabilidad y reactividad, pero también presenta desafíos en términos de diseño, gestión y monitoreo del sistema.

Ejemplo

Supongamos que tenemos un sistema donde los usuarios pueden realizar pedidos y recibir notificaciones cuando se envían los productos. Utilizaremos Kafka para enviar eventos relacionados con los pedidos y las notificaciones.

Primero, necesitamos configurar Kafka en nuestro proyecto, lo haremos utilizando Spring Boot:

import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.annotation.EnableKafka;

@Configuration
@EnableKafka
public class KafkaConfig {
    // Configuración de Kafka
}

Ahora, definimos un productor de eventos que enviará eventos de pedido a un tema de Kafka:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Component;

@Component
public class OrderEventProducer {

    private static final String TOPIC = "order-events";

    @Autowired
    private KafkaTemplate<String, String> kafkaTemplate;

    public void sendOrderEvent(String orderId) {
        kafkaTemplate.send(TOPIC, orderId);
    }
}

A continuación, creamos un consumidor de eventos que escuchará el topic de Kafka y procesará los eventos de pedidos:

import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;

@Component
public class OrderEventListener {

    @KafkaListener(topics = "order-events", groupId = "order-group")
    public void receiveOrderEvent(String orderId) {
        // Procesar el evento de pedido
        System.out.println("Pedido recibido: " + orderId);
        // Lógica para procesar el pedido, como enviar notificaciones, actualizar el estado del pedido, etc.
    }
}

Por último, en nuestro servicio de aplicación donde se realiza un pedido, podemos enviar un evento de pedido al productor de eventos:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class OrderService {

    private final OrderEventProducer orderEventProducer;

    @Autowired
    public OrderService(OrderEventProducer orderEventProducer) {
        this.orderEventProducer = orderEventProducer;
    }

    public void placeOrder(String orderId) {
        // Lógica para realizar el pedido
        
        // Enviar un evento de pedido
        orderEventProducer.sendOrderEvent(orderId);
    }
}

Vemos que cuando se realiza un pedido en el sistema, se envía un evento de pedido al topic de Kafka utilizando el productor de eventos. El consumidor de eventos escucha el topic de Kafka y procesa los eventos de pedido, como enviar notificaciones o actualizar el estado del pedido.

Este es solo un ejemplo simple para ilustrar cómo implementar un sistema basado en arquitectura orientada a eventos utilizando Java y Kafka. Dependiendo de tus requisitos específicos, puedes agregar más lógica y funcionalidades a tu sistema.

Aquí vemos un diagrama que muestra un ejemplo de una arquitectura basada en eventos (EDA) para otros casos de uso de un e-commerce:

Este ejemplo muestra tres componentes de productores de eventos y los eventos que producen. También vemos un enrutador de eventos que toma y filtra los eventos, luego envía estos eventos al consumidor de eventos pertinente.

La arquitectura basada en eventos permite que el sitio reaccione a los cambios de una variedad de fuentes durante los momentos de máxima demanda, sin bloquear la aplicación ni aprovisionar recursos en exceso.

¿Event Sourcing = EDA?

¡No!, Event Sourcing y EDA son conceptos relacionados, pero distintos en el ámbito de la ingeniería de software.

Event Sourcing es un patrón de diseño de software que consiste en almacenar un registro secuencial de todos los cambios que ocurren en el estado de una aplicación como una secuencia de eventos inmutables. Cada evento representa un cambio único y atómico en el estado de la aplicación y se almacena de manera secuencial en un registro de eventos. El objetivo principal del Event Sourcing es mantener un historial completo y auditado de todos los cambios en el estado de la aplicación y permitir la reconstrucción del estado actual de la aplicación en cualquier momento a partir de la reproducción de todos los eventos.

La arquitectura orientada a eventos es un enfoque arquitectónico que se centra en el intercambio de eventos entre los componentes de un sistema distribuido. En un sistema orientado a eventos, los componentes pueden emitir eventos cuando ocurren ciertas acciones o cambios de estado, y otros componentes pueden reaccionar a esos eventos y realizar acciones correspondientes. EDA se centra en la comunicación asincrónica y la independencia entre los componentes del sistema, lo que permite una mayor flexibilidad, escalabilidad y desacoplamiento.

Entre sus principales diferencias:

  • Enfoque principal: Event Sourcing se centra en el registro y almacenamiento de eventos para mantener un historial completo del estado de la aplicación, mientras que EDA se centra en la comunicación asincrónica entre componentes del sistema a través del intercambio de eventos.
  • Alcance de aplicación: Event Sourcing se aplica principalmente a la gestión del estado interno de una aplicación, mientras que EDA se puede aplicar a la comunicación entre sistemas, microservicios o componentes dentro de un sistema distribuido.
  • Naturaleza de los eventos: en Event Sourcing, los eventos representan cambios en el estado de la aplicación y son almacenados de manera inmutable. En EDA, los eventos pueden representar una variedad de acciones o notificaciones que ocurren en el sistema y pueden ser consumidos por varios componentes para realizar acciones específicas.

En resumen, aunque Event Sourcing y arquitectura orientada a eventos comparten similitudes en cuanto al uso de eventos, tienen enfoques y objetivos distintos en el diseño y la implementación de sistemas de software. ¿Tienes alguna duda o consulta? ¡Déjanos un comentario!

Cuéntanos qué te parece.

Los comentarios serán moderados. Serán visibles si aportan un argumento constructivo. Si no estás de acuerdo con algún punto, por favor, muestra tus opiniones de manera educada.