CodeGym /Cursos /Módulo 5. Spring /Eventos como "first-class citizens": estructura y ciclo d...

Eventos como "first-class citizens": estructura y ciclo de vida de los eventos

Módulo 5. Spring
Nivel 13 , Lección 6
Disponible

Ya nos hemos familiarizado con varias herramientas para trabajar con eventos, como Apache Kafka, RabbitMQ y ActiveMQ. Ahora es momento de profundizar en los detalles de los eventos como elemento clave de la arquitectura.


El papel de los eventos en la arquitectura

En una arquitectura Event-Driven los eventos no son solo mensajes que se pasan entre servicios. Son entidades completas (o, como se dice, "first-class citizens") alrededor de las cuales se construye la lógica de la aplicación. Si hacemos una analogía, un evento es como una carta que envías para avisar de que algo ocurrió. Pero no es solo el texto del mensaje: contiene todos los detalles importantes necesarios para que el receptor pueda procesarlo correctamente.

¿Por qué es importante? Cuando tratamos los eventos como "first-class citizens", empezamos automáticamente a prestar más atención a su calidad, estructura, contenido y al manejo de su ciclo de vida.

Ejemplo de la vida real

Imagínate que pides un café en tu cafetería favorita a través de una app.

  1. Tu acción (pedido) genera el evento OrderPlaced.
  2. El sistema procesa ese evento: el barista ve el pedido, la máquina de café empieza a prepararlo.
  3. Al final recibes una notificación de que tu café está listo (OrderCompleted).

Cada evento aporta su valor y tiene un tiempo de vida y un propósito concretos. Es importante que cada uno de ellos esté bien descrito y diseñado.


Estructura de un evento: atributos, metadatos y payload

Un evento normalmente consta de las siguientes partes:

  • Tipo (Type): el tipo de evento, por ejemplo, OrderPlaced, PaymentFailed, UserRegistered. Es el identificador de la acción que ocurrió.
  • Identificador (ID): identificador único del evento. Esto es importante para rastrear eventos en el sistema y evitar el procesamiento duplicado.
  • Marca temporal (Timestamp): cuándo exactamente ocurrió el evento. En sistemas distribuidos esto es crítico para entender el orden de procesamiento.
  • Origen (Source): quién o qué generó este evento. Por ejemplo, order-service, payment-gateway.
  • Carga útil (Payload): los datos que transporta el evento. Por ejemplo, para OrderPlaced puede ser información del pedido (ID del pedido, nombre del cliente, etc.).
  • Metadatos (Metadata): información adicional, por ejemplo, headers del mensaje, contexto de la llamada, etc.

Ejemplo de estructura JSON de un evento:

{
  "type": "OrderPlaced",
  "id": "123e4567-e89b-12d3-a456-426614174000",
  "timestamp": "2023-10-12T14:37:00Z",
  "source": "order-service",
  "payload": {
    "orderId": "98765",
    "customerId": "45678",
    "orderItems": [
      {
        "productId": "12345",
        "quantity": 2
      },
      {
        "productId": "67890",
        "quantity": 1
      }
    ],
    "totalPrice": 49.99
  },
  "metadata": {
    "traceId": "a1b2c3d4e5",
    "correlationId": "x5y6z7w8v9"
  }
}

Si la estructura del evento es difusa o poco clara, los suscriptores pueden tener problemas para procesarlo. Un evento bien estructurado facilita la interacción entre servicios, hace el sistema más predecible y más cómodo de mantener.

Consejo útil:

Procura usar estructuras unificadas para todos los eventos del sistema. Esto facilita mucho las pruebas y el procesamiento.


Ciclo de vida del evento

Un evento en la arquitectura EDA tiene su propio ciclo de vida. Vamos a ver las etapas principales:

Etapa 1: Generación del evento

El evento se crea en respuesta a una acción concreta. Por ejemplo, un usuario realizó un pedido — se genera el evento OrderPlaced. Normalmente esto ocurre en el servicio-fuente, que es el primero que sabe que ocurrió el evento.

Código en Java (Spring Boot):


@Component
public class OrderService {

    @Autowired
    private KafkaTemplate<String, OrderEvent> kafkaTemplate;

    public void placeOrder(Order order) {
        // Lógica de procesamiento del pedido
        OrderEvent event = new OrderEvent(
                "OrderPlaced",
                UUID.randomUUID().toString(),
                LocalDateTime.now().toString(),
                "order-service",
                order
        );
        kafkaTemplate.send("orders", event); // Envío del evento a Kafka
    }
}

Etapa 2: Traslado del evento

El evento generado se envía al sistema de entrega, por ejemplo, al broker de mensajes Kafka. Aquí es importante que el evento llegue al topic/cola correcto y sea procesado en tiempos fiables.

Etapa 3: Procesamiento del evento

Tras la recepción del evento por parte de los suscriptores, cada uno realiza su trabajo. Por ejemplo:

  • El servicio de pagos cobra el importe de la tarjeta.
  • El servicio de notificaciones envía un e-mail al cliente.

Código del suscriptor:


@Service
public class PaymentService {

    @KafkaListener(topics = "orders", groupId = "payment")
    public void handleOrderPlaced(OrderEvent event) {
        if ("OrderPlaced".equals(event.getType())) {
            System.out.println("Processing payment for order: " + event.getPayload().getOrderId());
            // Lógica de procesamiento del pago
        }
    }
}

Finalización del ciclo de vida

Tras un procesamiento exitoso, el evento puede ser archivado, marcado como procesado o incluso eliminado por completo si ya no tiene relevancia.


Crear eventos como entidades en el código

Para simplificar el trabajo con eventos tiene sentido crear una clase que represente el evento:


public class Event<T> {
    private String type;
    private String id;
    private String timestamp;
    private String source;
    private T payload;
    private Map<String, String> metadata;

    public Event(String type, String id, String timestamp, String source, T payload) {
        this.type = type;
        this.id = id;
        this.timestamp = timestamp;
        this.source = source;
        this.payload = payload;
        this.metadata = new HashMap<>();
    }

    // Getters y setters

    public void addMetadata(String key, String value) {
        this.metadata.put(key, value);
    }
}

Ejemplo de creación de un evento:

Order order = new Order(...);
Event<Order> event = new Event<>(
    "OrderPlaced",
    UUID.randomUUID().toString(),
    LocalDateTime.now().toString(),
    "order-service",
    order
);
event.addMetadata("traceId", "abc123");

Errores potenciales al diseñar eventos

Si los eventos no están descritos con suficiente claridad o no tienes un estándar para su procesamiento, pueden surgir los siguientes problemas:

  • Malentendidos entre equipos: los desarrolladores añaden datos a los eventos sin consenso común.
  • Información excesiva: los eventos se vuelven pesados y difíciles de procesar.
  • Pérdida de datos: si no controlas la unicidad de los eventos, los sistemas pueden procesar los mismos eventos varias veces.

Para evitar esto, es importante:

  1. Crear un "diccionario de eventos" unificado y sus estructuras.
  2. Estandarizar todos los eventos, de forma similar a una API.

Recomendaciones para diseñar y gestionar eventos

Principios de diseño:

  1. Minimalismo: guarda en el evento solo los datos necesarios para el procesamiento.
  2. Estabilidad: la estructura del evento no debe cambiar sin razones de peso.
  3. Transparencia: el evento debe describir de forma inequívoca lo que ocurrió.

Herramientas para monitorear eventos

Usa herramientas como Kafka UI, ELK Stack o Prometheus para seguir el ciclo de vida de los eventos y su procesamiento exitoso.

management:
  endpoints:
    web:
      exposure: include[ "prometheus", "health"]

Cuando los eventos se convierten en "first-class citizens", tu sistema se vuelve más organizado, predecible y, lo que es más importante, más fácil de mantener.

Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION