Event Sourcing es un enfoque para almacenar datos, donde la principal source of truth son los eventos. En lugar de guardar el estado actual del objeto, guardamos todos los cambios que alguna vez ocurrieron sobre él.
¿Cómo funciona esto? Imagina una cuenta bancaria. En vez de guardar, por ejemplo, el estado actual del balance, guardamos todas las operaciones: ingresos, retiros, transferencias. El estado actual del balance es el resultado de "reproducir" todas esas operaciones. Es como mirar el historial del navegador: para entender cómo llegaste a la página actual, simplemente revisas todas las páginas por las que has pasado antes.
Eventos:
1. Ingreso: +1000 u.e.
2. Retiro: -200 u.e.
3. Ingreso: +500 u.e.
Estado actual:
Saldo: 1300 u.e. (calculado a partir de eventos)
Esto permite no solo saber "dónde estás ahora", sino entender "cómo llegaste aquí".
Relación entre CQRS y Event Sourcing
Para entender cómo se relacionan estos dos enfoques, vamos a desglosarlos.
CQRS: dividir el mundo en dos
CQRS separa las operaciones del sistema en dos ámbitos: capa de comandos (Command) y capa de consultas (Query). Los comandos modifican el estado del sistema, y las consultas solo leen datos. Y aquí aparece una idea interesante: ¿y si para cambiar los datos (Command) usamos eventos?
Event Sourcing como una base potente para CQRS
Event Sourcing encaja perfectamente en la parte Command de CQRS. En lugar de guardar inmediatamente los cambios en la base de datos, guardas eventos que describen los cambios. Luego, sobre la base de esos eventos, actualizas el estado o creas nuevos modelos para la lectura de datos (Query).
Mira cómo se ve esto en acción:
- El usuario envía un comando
retirar dinero(por ejemplo, 500 u.e. de la cuenta). - En vez de actualizar inmediatamente el balance en la base de datos, el sistema crea un evento:
MoneyWithdrawn {amount: 500}. - Este evento se guarda en el almacén de eventos.
- El almacén de eventos luego procesa ese evento, actualizando los modelos de lectura (por ejemplo, "saldo del usuario").
Así que el principio principal es: "Primero el evento, luego las consecuencias".
Ventajas de su integración
Cuando combinas CQRS y Event Sourcing, obtienes varios bonos interesantes:
- Historial completo de cambios. Puedes retroceder en el tiempo y entender cómo un objeto llegó al estado actual. Esto es muy útil para auditoría o depuración.
- Posibilidad de volver a reproducir. Si necesitas recalcular el estado actual en otra base de datos o recuperar el sistema tras un fallo, simplemente reproduce todos los eventos.
- Separación automática de comandos y consultas. Event Sourcing encaja de forma natural con el enfoque CQRS. Los eventos son Commands, y los modelos preparados para consultas (Query) son tu salida legible.
- Asincronía. Un evento puede procesarse más tarde. Esto reduce la carga en la capa Command y permite que el sistema escale.
Implementación en la práctica
Vamos a implementar un ejemplo básico usando CQRS junto con Event Sourcing. Continuaremos con el desarrollo de nuestra aplicación de gestión de pedidos en una arquitectura de microservicios.
Supongamos que nuestra aplicación procesa pedidos. Necesitamos:
- Crear un pedido.
- Actualizar el estado del pedido.
- Preparar un modelo para el usuario, para que pueda ver el estado actual de todos sus pedidos.
Paso 1: Crear eventos
Crearemos clases Java que representan nuestros eventos.
// Evento de creación de pedido
public class OrderCreatedEvent {
private final String orderId;
private final String customerId;
private final String product;
public OrderCreatedEvent(String orderId, String customerId, String product) {
this.orderId = orderId;
this.customerId = customerId;
this.product = product;
}
// Getters
}
// Evento de actualización del estado del pedido
public class OrderStatusUpdatedEvent {
private final String orderId;
private final String status;
public OrderStatusUpdatedEvent(String orderId, String status) {
this.orderId = orderId;
this.status = status;
}
// Getters
}
Paso 2: Almacén de eventos
Necesitamos un lugar donde guardar los eventos. Para empezar, puede ser una lista simple.
import java.util.ArrayList;
import java.util.List;
public class EventStore {
private final List<Object> events = new ArrayList<>();
public void saveEvent(Object event) {
events.add(event);
}
public List<Object> getEvents() {
return new ArrayList<>(events);
}
}
Paso 3: Command Handler
El manejador de comandos se encarga de crear eventos a partir de los comandos entrantes.
import java.util.UUID;
public class OrderCommandHandler {
private final EventStore eventStore;
public OrderCommandHandler(EventStore eventStore) {
this.eventStore = eventStore;
}
public void handleCreateOrder(String customerId, String product) {
String orderId = UUID.randomUUID().toString();
OrderCreatedEvent event = new OrderCreatedEvent(orderId, customerId, product);
eventStore.saveEvent(event);
}
public void handleUpdateOrderStatus(String orderId, String status) {
OrderStatusUpdatedEvent event = new OrderStatusUpdatedEvent(orderId, status);
eventStore.saveEvent(event);
}
}
Paso 4: Query Layer
La capa de consultas procesa los eventos y prepara datos agregados para la lectura.
import java.util.HashMap;
import java.util.Map;
public class OrderQueryService {
private final Map<String, String> orderStatus = new HashMap<>();
public void applyEvent(Object event) {
if (event instanceof OrderCreatedEvent created) {
orderStatus.put(created.getOrderId(), "CREATED");
} else if (event instanceof OrderStatusUpdatedEvent updated) {
orderStatus.put(updated.getOrderId(), updated.getStatus());
}
}
public String getOrderStatus(String orderId) {
return orderStatus.get(orderId);
}
}
Paso 5: Integración
Ahora juntamos todo.
public class Main {
public static void main(String[] args) {
EventStore eventStore = new EventStore();
OrderCommandHandler commandHandler = new OrderCommandHandler(eventStore);
OrderQueryService queryService = new OrderQueryService();
// Creamos un pedido
commandHandler.handleCreateOrder("customer123", "Product A");
// Actualizamos el estado del pedido
String orderId = eventStore.getEvents()
.stream()
.filter(event -> event instanceof OrderCreatedEvent)
.map(event -> ((OrderCreatedEvent) event).getOrderId())
.findFirst()
.orElseThrow();
commandHandler.handleUpdateOrderStatus(orderId, "SHIPPED");
// Procesamos los eventos en el Query Service
eventStore.getEvents().forEach(queryService::applyEvent);
// Obtenemos el estado del pedido
System.out.println("Estado del pedido: " + queryService.getOrderStatus(orderId));
}
}
Principales desafíos y errores típicos
Problema de rendimiento
Con gran cantidad de eventos, reproducir todo el event log puede convertirse en un cuello de botella. ¿Solución? Usa snapshots — estados intermedios guardados.
Dificultad para depurar
Los sistemas orientados a eventos son más difíciles de depurar debido a su naturaleza asíncrona. Buenas trazas de logs y tracing distribuido (por ejemplo, Spring Sleuth) te salvarán la vida.
Consistencia de los datos
Es importante que comandos y eventos se procesen en el orden correcto. Si no, habrá caos.
CQRS y Event Sourcing son una pareja potente para microservicios escalables y tolerantes a fallos. Permiten separar la lógica de escritura y lectura, conservar el historial completo de acciones y trabajar con los datos de forma asíncrona. Si las configuras bien, serán tus aliadas, no un dolor de cabeza.
GO TO FULL VERSION