Marcio Cunha

Modelado de Dominios Complejos con Event Sourcing y Proyecciones de Lectura Asíncronas

Aprenda a estructurar sistemas empresariales complejos usando Event Sourcing para conservar el historial exacto de cambios y proyecciones asíncronas para consultas rápidas.

Marcio Cunha•7 min
También disponible en:PortuguêsEnglish
Resumen
  • El almacenamiento basado en eventos preserva el historial inmutable de todas las operaciones realizadas.
  • La separación estricta entre escritura y lectura evita la contención de recursos bajo cargas pesadas.
  • Las proyecciones asíncronas actualizan tablas optimizadas para consulta sin bloquear el flujo principal.
  • La consistencia eventual requiere elecciones de diseño deliberadas para manejar retrasos menores.
  • Reconstruir el estado actual desde cero simplifica la auditoría y la investigación de fallos.

El Desafío de Mapear Reglas de Negocio en Sistemas Modernos

Cuando construimos software para resolver problemas del mundo real, con frecuencia enfrentamos el dolor de comprimir flujos complejos en tablas relacionales tradicionales. En una aplicación convencional, solo guardamos la foto actual del estado de un registro, sobrescribiendo datos antiguos. En la práctica, esto significa que perdemos la narrativa temporal, el contexto que llevó a una decisión determinada y la capacidad de auditar el pasado con precisión. Para dominios donde cada transacción importa —como finanzas, logística y salud—, esta pérdida de historial crea un abismo entre el código y la realidad del negocio.

El modelado rico de dominios exige que el software hable el mismo idioma de los expertos en la materia. Cuando un modelo de datos se construye meramente para ahorrar espacio en disco, falla en reflejar comportamientos vitales. En lugar de preguntar solo cuál es el saldo actual de una cuenta, el sistema debe comprender la secuencia exacta de depósitos y retiros que generaron ese saldo. Es precisamente en este escenario de alta complejidad que los enfoques orientados a eventos ganan tracción, transformando nuestra forma de pensar sobre la persistencia y la consistencia de datos a escala.

Entendiendo Event Sourcing en la Práctica

Event Sourcing, o abastecimiento de eventos, es una estrategia arquitectónica donde no guardamos el estado final de un objeto, sino la lista cronológica de todos los hechos que le ocurrieron. Piense en un estado de cuenta bancario: no tiene solo el número '$1,500' guardado en una columna; tiene una lista de eventos como 'CuentaCreada', 'DepositoRealizado de $2,000' y 'RetiroRealizado de $500'. El estado actual no es más que una consecuencia matemática derivada de reproducir secuencialmente esos eventos desde el principio de los tiempos.

En la práctica, esto significa que la base de datos se convierte en un registro de solo adición, donde los registros nunca se actualizan ni se borran. Esta característica aporta una ventaja inmensa de concurrencia, ya que las escrituras en registros secuenciales generan muy poca contención en comparación con la actualización de filas individuales en tablas convencionales. Además, si descubrimos un error en una regla de negocio antigua, podemos corregir el código y reprocesar los eventos para recalcular el estado retroactivamente, garantizando una trazabilidad sin precedentes para el software.

Para ilustrar cómo manejamos estos eventos en el código, aquí hay un ejemplo en C# que muestra la estructura básica de un agregado aplicando un evento de negocio:

public class CuentaBancariaAggregate : AggregateRoot
{
    private decimal _saldo;
    private bool _activa;

    public void Aplicar(CuentaCreadaEvent evento)
    {
        _activa = true;
        _saldo = evento.SaldoInicial;
    }

    public void Aplicar(DinheiroDepositadoEvent evento)
    {
        if (!_activa) throw new InvalidOperationException("Cuenta inactiva.");
        _saldo += evento.Valor;
    }
}

El Papel Crucial de las Proyecciones de Lectura Asíncronas

Guardar solo eventos trae un desafío operacional evidente: ¿cómo mostrar rápidamente una lista de clientes o un saldo consolidado en la interfaz de usuario? Si tuviéramos que leer todo el historial de eventos de una cuenta cada vez que alguien abre la aplicación, el sistema se volvería insoportablemente lento. Aquí es donde entran las proyecciones de lectura asíncronas, que actúan como oyentes dedicados a traducir el flujo continuo de eventos en tablas optimizadas para consulta.

En la práctica, estos proyectores escuchan el bus de eventos en segundo plano, calculan el estado consolidado y escriben el resultado en una base de datos secundaria, como una base relacional estándar o un almacén de documentos NoSQL. Este concepto se alinea con el patrón CQRS, que separa la responsabilidad de escritura de la responsabilidad de lectura. Mientras que la escritura se enfoca en la validación rigurosa de las reglas de negocio y el almacenamiento seguro de hechos, la lectura se enfoca puramente en el rendimiento y la usabilidad de la interfaz gráfica.

Como este proceso ocurre fuera de la solicitud principal del usuario, es asíncrono. Esto introduce el concepto de consistencia eventual, lo que significa que puede haber un retraso de una fracción de segundo entre el momento en que se registra un evento y el momento en que aparece reflejado en las pantallas de consulta. Para la gran mayoría de las aplicaciones empresariales, este intercambio se compensa con creces por la escalabilidad y la robustez estructural alcanzadas.

Gestión de Concurrencia y Confiabilidad Distribuida

Cuando eliminamos el bloqueo tradicional de bases de datos y adoptamos arquitecturas orientadas a eventos, necesitamos gestionar la concurrencia de otra manera. Si dos usuarios intentan modificar el mismo agregado exactamente al mismo tiempo, ¿cómo evitamos que uno sobrescriba el trabajo del otro en silencio? La respuesta radica en el uso de control de concurrencia optimista mediante números de versión en cada flujo de eventos.

En la práctica, cada nuevo lote de eventos generado por un comando lleva el número de versión esperado del agregado. Cuando el mecanismo de persistencia intenta guardar estos eventos, verifica si la versión en la base de datos coincide con la versión enviada. Si otro proceso escribió algo en ese ínterin, la versión cambia, la transacción se rechaza por conflicto y se le indica a la aplicación cliente que reintente con los datos actualizados. Este modelo elimina los cuellos de botella de bloqueo pesimista, permitiendo que miles de operaciones ocurran simultáneamente sin bloquear conexiones.

A continuación, un ejemplo en Node.js que demuestra cómo un manejador de eventos actualiza una proyección de lectura en una base de datos relacional ligera:

async function handleDineroDepositado(evento, dbPool) {
    const query = `UPDATE saldos_clientes SET saldo = saldo + $1, actualizado_en = NOW() WHERE cliente_id = $2`;
    await dbPool.query(query, [evento.valor, evento.clienteId]);
}

Este tipo de rutina garantiza que el modelo de lectura permanezca sincronizado con el bus de mensajes, operando de forma desacoplada y resiliente ante caídas momentáneas de microservicios dependientes. Para complementar esta implementación en entornos de producción, consulte también nuestros análisis técnicos sobre Modelado de Dominios con Event Sourcing y Proyecciones de Lectura y Modelado de Dominios con Event Sourcing y Proyecciones CQRS Asíncronas en NoSQL.

Estrategias para Migración y Evolución de Esquemas

Mantener un sistema basado en eventos durante varios años introduce un desafío único: ¿qué sucede cuando la estructura de un evento necesita cambiar? En las bases de datos relacionales tradicionales, alteramos una columna con un comando de migración y el problema se resuelve. En Event Sourcing, los eventos almacenados en el pasado son inmutables; no se puede simplemente reescribir el historial de transacciones ocurridas en producción hace tres años.

Para resolver este dilema, utilizamos estrategias de versionado y upcasting. El upcasting consiste en interceptar eventos antiguos a medida que se leen del almacenamiento y transformarlos dinámicamente a la estructura de la versión más reciente antes de pasarlos al dominio. De esta manera, el núcleo de la aplicación maneja solo la versión actualizada de los objetos, mientras el archivo histórico permanece intacto y fiel a lo que realmente sucedió en el momento de la emisión original.

Otro punto crítico es la limpieza y el snapshotting. Como el historial de un agregado puede crecer indefinidamente, reprocesar miles de eventos en cada carga de objeto puede degradar el rendimiento. La solución es crear instantáneas periódicas del estado consolidado, permitiendo que la aplicación lea la última instantánea y procese solo los pocos eventos generados después de ella, uniendo lo mejor de ambos mundos: auditoría completa y alta velocidad de ejecución.

Adotar el modelado de dominios complejos con Event Sourcing y proyecciones de lectura asíncronas requiere madurez técnica y claridad sobre los problemas que intenta resolver. No es una bala de plata aplicable a cualquier CRUD simple, sino una herramienta poderosa para dominios ricos donde la trazabilidad, la auditoría rigurosa y la escalabilidad de lectura y escritura justifican el esfuerzo inicial de implementación.

Al separar la complejidad transaccional del almacenamiento de hechos de la agilidad de las proyecciones de lectura, construimos sistemas altamente resilientes, capaces de evolucionar junto con las demandas del negocio sin corromper el pasado. La inversión en dominar estos patrones arquitectónicos paga dividendos duraderos en forma de software más transparente, fácil de depurar y preparado para crecer sin fricción.