Event sourcing sin la ceremonia que la mayoría nunca necesitó
Event sourcing sin ceremonia no es arquitectura de conferencia. La mayoría de los equipos necesita un log de eventos append-only, una tabla de proyección y límites de escritura claros — no clusters Kafka ni middleware CQRS antes de tener usuarios.
Event sourcing sin ceremonia empieza con una pregunta que muchas revisiones de arquitectura omiten: ¿qué problema resuelve el event log que una tabla normal no puede? La respuesta de manual — historial completo, consultas temporales, estado reproducible — es real. La implementación de manual — event store dedicado, pipeline de snapshots, workers de proyección, esquemas versionados, orquestación de sagas — suele ser desproporcionada. Equipos adoptan la etiqueta antes de necesitar la maquinaria. Seis meses después mantienen tres write paths, dos buses de mensajes y un job de replay en el que nadie confía, todo para responder "¿quién cambió el estado de esta factura?" — una pregunta que una tabla de auditoría append-only responde en cuarenta líneas de SQL.
El terreno útil es el event sourcing liviano: tratar mutaciones de dominio como eventos inmutables, proyectar estado actual en tablas optimizadas para consulta, y diferir infraestructura pesada hasta que volumen de replay, alcance de compliance o coreografía entre servicios lo exijan de verdad. El patrón conserva el beneficio central — historial auditable y estado reconstruible — sin pagar el impuesto de sistemas distribuidos el día uno.
El event sourcing completo falla cuando la ceremonia llega antes que la demanda
El event sourcing clásico prescribe un stack. Comandos entran a un agregado. El agregado emite eventos. Los eventos llegan a un store append-only. Proyecciones consumen el stream y construyen read models. Snapshots aceleran la recarga del agregado. Process managers coordinan workflows multi-paso entre agregados. Cada capa resuelve un problema real a escala. Cada capa también suma superficie operativa: reglas de evolución de esquema, consumidores idempotentes, manejo de poison messages, monitoreo de lag de proyección y runbooks de on-call para fallos de replay.
Equipos chicos chocan cuando implementan el stack completo para un solo bounded context dentro de un monolito. El write model se vuelve una isla event-sourced en un mar CRUD. Desarrolladores deben recordar qué tablas son proyecciones (nunca actualizar directo) y cuáles son legacy (actualizar libremente). Depurar un bug visible al usuario exige rastrear desde el read model hasta el evento que causó la divergencia — a menudo entre tres repos y un Kafka local.
El modo de fallo no es teórico. Un módulo de billing event-sourced "por flexibilidad futura" mientras suscripciones, metering y facturación siguen en CRUD produce semántica split-brain: reembolsos aparecen en el log pero el dashboard lee una vista materializada con cinco minutos de retraso. Tickets de soporte reportan "el sistema dice reembolsado pero el saldo está mal." El equipo pasa un sprint construyendo infra de snapshots en lugar de arreglar el bug de proyección.
Event sourcing sin ceremonia significa que el log es audit trail primero y religión arquitectónica después.
El diagnóstico es simple. Si el consumidor principal del stream es un auditor de compliance o un investigador de incidentes — no una flota de proyecciones en tiempo real — el equipo necesita un audit log, no event sourcing. Si el consumidor principal es un read model dentro del mismo deployable, necesita una tabla append-only y una función de proyección, no un message bus.
Event sourcing liviano es un log append-only más una proyección
El patrón mínimo viable tiene tres piezas.
Una tabla de eventos append-only. Cada fila registra qué pasó: tipo de evento, identificador del agregado, payload en JSONB, actor, timestamp y opcionalmente secuencia monotónica o hash chain para evidencia de tampering. Sin updates. Sin deletes salvo archivo con retención gobernada. Postgres lo maneja con una tabla, índices BRIN en occurred_at para rangos temporales e índices GIN en claves extraídas del payload para búsquedas por entidad.
Una proyección al estado actual. Una función — trigger, worker en background o paso inline en la transacción — aplica cada evento a una tabla optimizada para consulta. InvoiceCreated inserta una fila. InvoicePaid actualiza status y paid_at. La tabla de proyección es lo que lee la aplicación. La tabla de eventos es lo que leen investigadores y jobs de replay.
Un límite de escritura. El código de aplicación nunca muta tablas de proyección excepto aplicando eventos. Esa regla se impone por convención en un monolito (writers privados al módulo) o por permisos de base de datos en setups más estrictos. Un punto de entrada emite eventos; un camino de código los aplica.
CREATE TABLE domain_events (
id BIGSERIAL PRIMARY KEY,
aggregate_id UUID NOT NULL,
event_type TEXT NOT NULL,
payload JSONB NOT NULL,
actor_id UUID,
occurred_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX domain_events_aggregate_idx
ON domain_events (aggregate_id, occurred_at);
CREATE TABLE invoices (
id UUID PRIMARY KEY,
status TEXT NOT NULL,
amount_cents INTEGER NOT NULL,
paid_at TIMESTAMPTZ
);
INSERT INTO domain_events (aggregate_id, event_type, payload, actor_id)
VALUES ($1, 'InvoicePaid', '{"paid_at": "2026-07-10T12:00:00Z"}', $2);
UPDATE invoices
SET status = 'paid', paid_at = '2026-07-10T12:00:00Z'
WHERE id = $1;Esto no es CQRS. No son microservicios. Es un registro honesto de mutaciones con una superficie de lectura derivada — el mismo patrón que los marcos de compliance describen como audit trail inmutable, extendido con estructura suficiente para reconstruir estado si la proyección deriva.
La arquitectura de monolito modular hace natural este patrón: la tabla de eventos vive en el schema del módulo dueño, las tablas de proyección son privadas al módulo, y otros módulos ven solo la API pública de consulta. Los límites permanecen enforced por el lenguaje hasta que la extracción requiera red de verdad.
Tres señales que justifican más maquinaria
No todo sistema se detiene en el patrón mínimo. Tres señales indican que el stack con ceremonia vale su costo.
Replay a escala. Reconstruir proyecciones desde cero debe completarse en una ventana de recuperación definida — horas, no días. Un millón de eventos replayed por triggers síncronos no cumple ese SLA. Snapshot stores y workers de proyección en background se justifican cuando el replay es operación de producción, no conveniencia de desarrollo.
Coreografía entre servicios. Múltiples deployables deben reaccionar al mismo stream con garantías de orden. Una tabla append in-process no coordina un servicio de pagos, un worker de fulfillment y un servicio de notificaciones. Un log durable con consumer groups entra cuando el límite de red es real, no anticipado.
Inmutabilidad regulatoria con prueba externa. SOC 2, HIPAA y registros financieros a veces exigen evidencia de tampering más allá de permisos de base de datos. Filas con hash chain, exports WORM o anclaje externo justifican infra adicional cuando auditores piden prueba criptográfica, no solo controles de acceso.
Hasta que una de estas señales esté presente, agregar Kafka es especulación. Agregar versionado de snapshots es especulación. El log liviano maneja los primeros 100k eventos y el primer cuestionario de compliance.
Evolución de esquema sin framework de versionado de eventos
El problema práctico más duro en event sourcing no es almacenamiento — es cambio de esquema. Los eventos son inmutables. Payloads escritos hace dos años aún deben deserializarse hoy.
Event sourcing liviano maneja evolución con convenciones, no frameworks.
Upcast al leer. Guardar eventos en su forma original. Al replay o proyectar, una función pequeña migra payloads InvoiceCreatedV1 a la forma actual antes de aplicar. La versión vive en sufijo de event_type o columna schema_version.
Cambios aditivos en vuelo. Campos nuevos son opcionales con defaults. Campos removidos permanecen en eventos viejos pero son ignorados por código de proyección nuevo. Renombres breaking pasan por un tipo de evento nuevo, no mutación in-place.
Rebuilds de proyección como válvula de escape. Si la lógica de proyección cambia materialmente, truncar la tabla de proyección y replay desde el log. Con menos de un millón de eventos esto completa en minutos en Postgres. El script de rebuild es el test de integración para evolución de esquema — si el replay falla, la migración no es segura.
Evitar proliferación prematura de tipos de evento. UserEmailChanged, UserEmailChangedV2 y UserEmailCorrected en el mismo trimestre significa que el modelo de dominio no era estable suficiente para event sourcing aún. Arreglar el modelo antes que el log.
¿Cuándo gana event sourcing liviano frente a CRUD más triggers de auditoría?
La decisión no es ideológica. Es operativa.
¿Cuál es el patrón de lectura principal?
Si la aplicación siempre consulta estado actual y ocasionalmente necesita historial para soporte, CRUD con trigger de auditoría en la misma tabla es más simple. Triggers de Postgres capturan old_value y new_value JSONB en cada update — suficiente para "¿quién cambió esta fila?" Event sourcing liviano gana cuando el historial es superficie de producto de primera clase: activity feeds, undo, reportes temporales o reconstrucción de estado tras bugs de proyección.
¿Puede el equipo imponer un solo write path?
Event sourcing falla cuando desarrolladores bypass el log y escriben directo a tablas de proyección "solo esta vez." Un monolito modular con módulos package-private puede imponer el límite. Un codebase sin ownership no puede — y acumulará drift de proyección más rápido que CRUD con triggers.
¿El replay debe ser automatizado o alcanza export de auditoría?
Exports de compliance sobre doce meses no requieren infra de replay. Requieren filas indexadas e inmutables consultables por actor_id y occurred_at. Infra de replay se justifica cuando proyecciones incorrectas deben ser reconstruibles sin cirugía SQL manual — típicamente después de que el equipo haya enviado al menos un bug de proyección a producción y sentido el costo de recuperación.
Una visión opuesta
Un argumento frecuente sostiene que event sourcing parcial es peor que ninguno — que equipos deben comprometerse al patrón completo con tooling adecuado o quedarse en CRUD, porque un event store a medias genera más confusión que un audit log disciplinado.
Ese argumento es correcto para equipos sin disciplina de enforcement. Una tabla de proyección actualizada desde tres code paths es peor que CRUD honesto. Es incorrecto para equipos que ya imponen límites de módulo y necesitan estado reconstruible dentro de un deployable. Event sourcing liviano es el camino intermedio para bounded contexts donde el historial importa pero aún no existe message bus.
El modo de fallo a evitar es etiquetar tablas CRUD como "events" sin semántica append-only. Una tabla events con ON UPDATE CASCADE no es event sourcing. Es un audit log mal nombrado que fallará la primera auditoría de inmutabilidad.
Lo que importa recordar
- Event sourcing sin ceremonia es log append-only, tabla de proyección y un solo límite de escritura — no message bus por defecto.
- Si el consumidor principal del stream es compliance o investigación de incidentes, empezar con audit log.
- Kafka, snapshots y orquestación de sagas valen su costo a escala de replay, coreografía entre servicios o requisitos de inmutabilidad criptográfica.
- Evolución de esquema usa funciones upcast y rebuilds de proyección — no frameworks de versionado prematuros.
- El monolito modular es el host natural: un módulo posee el log, la proyección y la API pública de consulta.
- Event sourcing parcial sin disciplina de write path crea estado split-brain peor que CRUD con triggers.
Conclusión
Event sourcing se volvió sinónimo de infraestructura pesada enough para necesitar su propio platform team. Esa asociación empujó a equipos hacia adopción heroica o evitación total. El medio productivo es más chico: registrar qué pasó, derivar qué es verdad ahora, y agregar maquinaria cuando el dolor medido — tiempo de replay, orden entre servicios, rigor de auditoría — supere lo que Postgres y un límite de módulo disciplinado pueden cargar.
La próxima revisión de arquitectura no debería preguntar "¿debemos event-source?" Debería preguntar qué mutaciones necesitan historial inmutable, qué consultas necesitan estado proyectado, y si alguna señal en el horizonte justifica un log durable más allá de la base que la aplicación ya corre. La mayoría de sistemas en etapa temprana responden con una tabla append-only y una regla clara sobre quién puede tocar el read model. Eso es ceremonia suficiente por mucho tiempo.


