Si vas a hacer event sourcing sobre MongoDB, hay dos requisitos que no aparecen en ningún tutorial y que te van a bloquear en la primera hora. Los dos tienen arreglo trivial; encontrarlos es lo que cuesta.
1. Las transacciones exigen un replica set
Un event store escribe el evento y actualiza la versión del agregado. Esas dos cosas tienen que ser atómicas, o pierdes el control de concurrencia optimista. Así que necesitas una transacción.
Y las transacciones de MongoDB exigen un conjunto de réplicas. Un mongod suelto falla en el primer commit, con un error que menciona sesiones y no menciona réplicas.
Para desarrollo, un nodo único en modo réplica basta:
docker run -d --name mongo -p 27017:27017 mongo:7 --replSet rs0
docker exec mongo mongosh --eval \'rs.initiate()\'
Un solo nodo, pero declarado como réplica. Eso es todo lo que MongoDB necesita para habilitar transacciones.
2. No se puede crear una colección dentro de una transacción
Éste es peor, porque sólo ocurre la primera vez.
MongoDB crea las colecciones al vuelo en la primera escritura. Pero dentro de una transacción multi-documento, esa creación implícita está prohibida. Así que con la base vacía, tu primer append falla — y en cuanto alguien crea la colección por otra vía, deja de fallar para siempre.
El resultado es un bug que no se reproduce en el equipo de nadie salvo en el de quien acaba de clonar el proyecto. Y en producción aparece exactamente una vez, en el despliegue inicial, cuando menos ganas tienes de depurar.
La solución es crear las colecciones al arrancar, fuera de cualquier transacción:
async onApplicationBootstrap(): Promise<void> {
const existentes = await this.connection.db.listCollections().toArray();
const nombres = new Set(existentes.map((c) => c.name));
for (const requerida of [\'events\', \'snapshots\']) {
if (!nombres.has(requerida)) {
await this.connection.db.createCollection(requerida);
}
}
}
Un tercero, de regalo: commit() no espera
El commit() de @nestjs/cqrs es síncrono y sin espera: entrega los eventos al bus y vuelve. No devuelve una promesa que puedas esperar.
En producción casi nunca importa. En un test, importa siempre: escribes el comando, compruebas la proyección, y la proyección todavía no ha corrido. El test falla una de cada cinco veces y culpas a la máquina.
await this.publisher.flush(); // espera a que los proyectores terminen
Y una advertencia sobre cómo diagnosticar esto: instrumentar puede esconder la carrera. Añadir una consulta entre la escritura y la comprobación introduce suficiente latencia para que el proyector llegue a tiempo, y el test pasa cinco de cinco. Quitas la instrumentación y vuelve a fallar. Mide sin tocar el camino que estás midiendo.
Para seguir
La configuración completa del event store, con snapshots y upcasting, está en la documentación de ddd-es-lib.
