{"schemaVersion":1,"id":"https://lucioduran.com/es/blog/event-sourcing-cqrs-production-lessons","url":"https://lucioduran.com/es/blog/event-sourcing-cqrs-production-lessons","title":"Event Sourcing + CQRS: Diseño de Event Store, Proyecciones, Snapshots y Cuándo NO Usarlo","description":"Lecciones de producción en la construcción de sistemas event-sourced, diseño de event store, rebuilds de proyecciones, estrategias de snapshots, orquestación de sagas, y una evaluación honesta de cuándo event sourcing empeora las cosas.","language":"es","datePublished":"2026-02-22","dateModified":null,"contentFormat":"text/markdown","content":"Architecture\n\n# Event Sourcing + CQRS: Diseño de Event Store, Proyecciones, Snapshots y Cuándo NO Usarlo\n\n2026-02-226 min lectura\n\nLucio Durán\nEngineering Manager & AI Solutions Architect\n\nTambién disponible en: [English](<https://lucioduran.com/blog/event-sourcing-cqrs-production-lessons>)\n\n## Fundamentos de Event Sourcing\n\nEn lugar de almacenar el estado actual de una entidad, se almacena la secuencia de eventos que condujeron a ese estado. El estado actual se deriva reproduciendo los eventos. Este patrón se implementa típicamente con [PostgreSQL](<https://www.postgresql.org/>) o un event store dedicado.\n\n```python\n# event_store.py\nfrom dataclasses import dataclass, field\nfrom datetime import datetime\nimport json, uuid\n\n@dataclass(frozen=True)\nclass Event:\n event_id: str\n stream_id: str\n event_type: str\n data: dict\n metadata: dict\n version: int\n timestamp: datetime\n schema_version: int = 1\n\nclass EventStore:\n def __init__(self, connection):\n self.conn = connection\n\n def append(self, stream_id: str, events: list[Event],\n expected_version: int) -> None:\n with self.conn.cursor() as cur:\n cur.execute(\n \"SELECT MAX(version) FROM events WHERE stream_id = %s FOR UPDATE\",\n (stream_id,)\n )\n current_version = cur.fetchone()[0] or 0\n if current_version != expected_version:\n raise ConcurrencyError(\n f\"Versión esperada {expected_version}, \"\n f\"obtuve {current_version} para stream {stream_id}\"\n )\n for i, event in enumerate(events):\n new_version = expected_version + i + 1\n cur.execute(\"\"\"\n INSERT INTO events\n (event_id, stream_id, event_type, data, metadata,\n version, timestamp, schema_version)\n VALUES (%s, %s, %s, %s, %s, %s, %s, %s)\n \"\"\", (\n event.event_id, stream_id, event.event_type,\n json.dumps(event.data), json.dumps(event.metadata),\n new_version, event.timestamp, event.schema_version\n ))\n self.conn.commit()\n```\n\n### El Schema del Event Store\n\n```sql\nCREATE TABLE events (\n event_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),\n stream_id TEXT NOT NULL,\n event_type TEXT NOT NULL,\n data JSONB NOT NULL,\n metadata JSONB NOT NULL DEFAULT '{}',\n version INTEGER NOT NULL,\n timestamp TIMESTAMPTZ NOT NULL DEFAULT now(),\n schema_version INTEGER NOT NULL DEFAULT 1,\n UNIQUE (stream_id, version)\n);\n\nCREATE INDEX idx_events_stream ON events (stream_id, version);\nCREATE INDEX idx_events_type ON events (event_type, timestamp);\n\nCREATE TABLE snapshots (\n stream_id TEXT NOT NULL,\n version INTEGER NOT NULL,\n state JSONB NOT NULL,\n created_at TIMESTAMPTZ NOT NULL DEFAULT now(),\n PRIMARY KEY (stream_id, version)\n);\n```\n\n## Agregados: Donde Vive la Lógica de Negocio\n\n```python\n# order_aggregate.py\nclass OrderAggregate:\n def __init__(self):\n self.order_id = None\n self.status = None\n self.items = []\n self.total = 0\n self.version = 0\n\n def place_order(self, order_id, items, customer_id):\n if self.status is not None:\n raise InvalidOperationError(\"La orden ya existe\")\n if not items:\n raise ValidationError(\"La orden debe tener al menos un item\")\n total = sum(item['price'] * item['quantity'] for item in items)\n return [Event(\n event_id=str(uuid.uuid4()),\n stream_id=f\"order-{order_id}\",\n event_type=\"OrderPlaced\",\n data={'order_id': order_id, 'customer_id': customer_id,\n 'items': items, 'total': total},\n metadata={'causation_id': str(uuid.uuid4())},\n version=self.version + 1,\n timestamp=datetime.utcnow()\n )]\n\n def apply(self, event: Event):\n handler = getattr(self, f'_on_{event.event_type}', None)\n if handler:\n handler(event.data)\n self.version = event.version\n\n def _on_OrderPlaced(self, data):\n self.order_id = data['order_id']\n self.status = 'placed'\n self.items = data['items']\n self.total = data['total']\n\n def _on_PaymentConfirmed(self, data):\n self.status = 'paid'\n\n def _on_OrderCancelled(self, data):\n self.status = 'cancelled'\n\n @classmethod\n def from_events(cls, events):\n aggregate = cls()\n for event in events:\n aggregate.apply(event)\n return aggregate\n```\n\n## Proyecciones: Construyendo Read Models\n\nLas proyecciones transforman el event log en read models optimizados para queries. Esta es la \"Q\" en CQRS.\n\n```python\nclass OrderDashboardProjection:\n def __init__(self, read_db):\n self.db = read_db\n\n def handle(self, event: Event):\n handler = getattr(self, f'_handle_{event.event_type}', None)\n if handler:\n handler(event)\n\n def _handle_OrderPlaced(self, event):\n data = event.data\n self.db.execute(\"\"\"\n INSERT INTO order_dashboard\n (order_id, customer_id, status, total, item_count, placed_at)\n VALUES (%s, %s, 'placed', %s, %s, %s)\n ON CONFLICT (order_id) DO UPDATE SET\n status = 'placed', total = EXCLUDED.total\n \"\"\", (data['order_id'], data['customer_id'],\n data['total'], len(data['items']), event.timestamp))\n\n def rebuild(self, event_store):\n \"\"\"Reconstruir toda la proyección desde cero.\"\"\"\n self.db.execute(\"TRUNCATE order_dashboard\")\n for batch in event_store.read_all(batch_size=1000):\n for event in batch:\n self.handle(event)\n self.db.commit()\n```\n\n## Snapshots: Domando el Costo de Rehidratación\n\n```python\nclass SnapshotRepository:\n def __init__(self, event_store, snapshot_store, interval=100):\n self.event_store = event_store\n self.snapshot_store = snapshot_store\n self.interval = interval\n\n def load(self, stream_id):\n snapshot = self.snapshot_store.get_latest(stream_id)\n if snapshot:\n aggregate = OrderAggregate()\n aggregate.__dict__.update(snapshot['state'])\n aggregate.version = snapshot['version']\n events = self.event_store.read_stream(stream_id, from_version=snapshot['version'])\n else:\n aggregate = OrderAggregate()\n events = self.event_store.read_stream(stream_id)\n\n for event in events:\n aggregate.apply(event)\n return aggregate\n\n def save(self, stream_id, aggregate, new_events):\n self.event_store.append(stream_id, new_events, aggregate.version - len(new_events))\n if aggregate.version % self.interval == 0:\n self.snapshot_store.save(\n stream_id=stream_id, version=aggregate.version,\n state=aggregate.__dict__\n )\n```\n\n## Cuándo NO Usar Event Sourcing\n\nEsta sección aborda los escenarios comunes donde event sourcing empeora las cosas.\n\n**1. Aplicaciones CRUD simples.** Si tu dominio es \"usuario llena formulario, datos van a la base, alguien los lee después,\" event sourcing agrega complejidad sin beneficio. Una tabla PostgreSQL con columnas ` created_at ` y ` updated_at ` cubre el 90% de las necesidades de auditoría.\n\n**2. Cuando tu equipo no entiende DDD.** Event sourcing sin boundaries de agregados correctos se vuelve una pesadilla. Si tu equipo no puede responder \"cuál es el boundary de consistencia aquí?\", se va a terminar con un monolito distribuido que es más difícil de razonar que la base de datos que reemplazó.\n\n**3. Cuando se necesita queries ad-hoc.** Los event stores están optimizados para leer un solo stream. Queries cross-aggregate requieren proyecciones. Si tu caso de uso principal es \"mostrame todas las órdenes del cliente X de marzo que contengan producto Y,\" se necesita una proyección para cada nuevo patrón de query. Con una base relacional, es una cláusula WHERE.\n\n**4. Prototipos y MVPs.** El tiempo que se invierte construyendo infraestructura de event store, rebuild de proyecciones, estrategias de snapshots y upcasting es tiempo que no se está gastando validando tu hipótesis de negocio.\n\n**5. Cuando la consistencia eventual es inaceptable.** Las proyecciones son eventualmente consistentes con el event store. Siempre hay un delay entre que un evento se almacena y el read model se actualiza.\n\nUn ejemplo ilustrativo: un servicio de gestión de usuarios donde los usuarios se registran, actualizan su perfil y cambian su email. Con quizás cinco tipos de eventos, el requerimiento de \"audit trail\" que motivó event sourcing se podría haber resuelto con una simple tabla ` user_audit_log `. En cambio, el sistema tenía un event store, tres proyecciones, una estrategia de snapshots, y un pipeline de upcasting para cuando se agregó un campo ` display_name `. Arquitecturalmente interesante y prácticamente terrible.\n\n## Cuándo Event Sourcing Brilla\n\nLos sistemas donde event sourcing tiene éxito tienden a compartir rasgos comunes:\n\n- **Lógica de dominio compleja** con muchas transiciones de estado y reglas de negocio\n- **Requerimientos de auditoría** que iban más allá de \"quién cambió qué\" a \"cuál era el estado en el momento T y por qué\"\n- **Integraciones event-driven** donde sistemas downstream necesitaban reaccionar a eventos de dominio\n- **Queries temporales** como \"mostrame la orden como estaba ayer a las 3pm\"\n\nConsidérese una plataforma de trading donde requisitos regulatorios demandan un audit trail completo e inmutable de cada cambio de estado. O un sistema de logística donde docenas de servicios downstream necesitan reaccionar a eventos de envío. En ambos casos, event sourcing es el fit natural, y la alternativa (change data capture sobre una base relacional) sería más compleja, no menos.\n\nLa realidad es que la mayoría de los sistemas no necesitan event sourcing. Pero cuando el caso de uso lo demanda, nada más se acerca.\n\n## Preguntas frecuentes\n\n### ¿Cuándo NO debería usar event sourcing?\n\nAplicaciones CRUD simples, sistemas donde las pistas de auditoría no son importantes, equipos sin experiencia en DDD, prototipos y MVPs, y cualquier sistema donde la complejidad agregada no se justifica por los beneficios. La mayoría de las aplicaciones se benefician más de una base de datos tradicional con change data capture para necesidades de auditoría.\n\n### ¿Cuál es la diferencia entre event sourcing y arquitectura event-driven?\n\nLa arquitectura event-driven usa eventos para comunicación entre servicios. Event sourcing almacena estado como una secuencia de eventos. Es posible tener arquitectura event-driven sin event sourcing (la mayoría lo hace), y event sourcing sin arquitectura event-driven (ES de servicio único).\n\n### ¿Cómo manejar la evolución de esquemas en un event store?\n\nTres enfoques: upcasting (transformar eventos anteriores al esquema nuevo en lectura), handlers de eventos versionados (manejar v1 y v2 en las proyecciones), o copy-transform (migrar eventos a un store nuevo con el esquema nuevo). Upcasting es el más común y menos disruptivo.\n\n## Tips\n\n1. **Tip:** Almacenar los eventos como hechos inmutables con campos ` schema_version `. Los esquemas de eventos cambiarán, el upcasting de v1 a v2 es mucho más sencillo que reescribir el event store.\n2. **Tip:** Tomar snapshot cada 100-200 eventos por agregado. Sin snapshots, rehidratar un agregado con 10K eventos tarda segundos, los usuarios lo notan.\n3. **Tip:** Las proyecciones deben ser idempotentes y reconstruibles desde cero. Si no es posible eliminar una proyección y reconstruirla desde el event log, la arquitectura tiene una dependencia oculta.\n4. **Tip:** Utilizar ` optimistic concurrency ` en la versión del stream de eventos. Si dos comandos intentan agregar eventos al mismo stream simultáneamente, el segundo debe fallar y reintentar, ese es el límite de consistencia.\n\n- event-sourcing\n- cqrs\n- event-store\n- projections\n- sagas\n- architecture\n- ddd\n\n[Volver al Blog](<https://lucioduran.com/es/blog>)","headings":[{"level":1,"text":"Event Sourcing + CQRS: Diseño de Event Store, Proyecciones, Snapshots y Cuándo NO Usarlo","url":"https://lucioduran.com/es/blog/event-sourcing-cqrs-production-lessons#title"},{"level":2,"text":"Fundamentos de Event Sourcing","url":"https://lucioduran.com/es/blog/event-sourcing-cqrs-production-lessons#fundamentos-de-event-sourcing"},{"level":3,"text":"El Schema del Event Store","url":"https://lucioduran.com/es/blog/event-sourcing-cqrs-production-lessons#el-schema-del-event-store"},{"level":2,"text":"Agregados: Donde Vive la Lógica de Negocio","url":"https://lucioduran.com/es/blog/event-sourcing-cqrs-production-lessons#agregados-donde-vive-la-lgica-de-negocio"},{"level":2,"text":"Proyecciones: Construyendo Read Models","url":"https://lucioduran.com/es/blog/event-sourcing-cqrs-production-lessons#proyecciones-construyendo-read-models"},{"level":2,"text":"Snapshots: Domando el Costo de Rehidratación","url":"https://lucioduran.com/es/blog/event-sourcing-cqrs-production-lessons#snapshots-domando-el-costo-de-rehidratacin"},{"level":2,"text":"Cuándo NO Usar Event Sourcing","url":"https://lucioduran.com/es/blog/event-sourcing-cqrs-production-lessons#cundo-no-usar-event-sourcing"},{"level":2,"text":"Cuándo Event Sourcing Brilla","url":"https://lucioduran.com/es/blog/event-sourcing-cqrs-production-lessons#cundo-event-sourcing-brilla"},{"level":2,"text":"Preguntas frecuentes","url":"https://lucioduran.com/es/blog/event-sourcing-cqrs-production-lessons#questions"},{"level":3,"text":"¿Cuándo NO debería usar event sourcing?","url":"https://lucioduran.com/es/blog/event-sourcing-cqrs-production-lessons#question-1"},{"level":3,"text":"¿Cuál es la diferencia entre event sourcing y arquitectura event-driven?","url":"https://lucioduran.com/es/blog/event-sourcing-cqrs-production-lessons#question-2"},{"level":3,"text":"¿Cómo manejar la evolución de esquemas en un event store?","url":"https://lucioduran.com/es/blog/event-sourcing-cqrs-production-lessons#question-3"},{"level":2,"text":"Tips","url":"https://lucioduran.com/es/blog/event-sourcing-cqrs-production-lessons#article-tips"}],"sections":[{"heading":"Event Sourcing + CQRS: Diseño de Event Store, Proyecciones, Snapshots y Cuándo NO Usarlo","level":1,"url":"https://lucioduran.com/es/blog/event-sourcing-cqrs-production-lessons","content":"Architecture"},{"heading":"Event Sourcing + CQRS: Diseño de Event Store, Proyecciones, Snapshots y Cuándo NO Usarlo","level":1,"url":"https://lucioduran.com/es/blog/event-sourcing-cqrs-production-lessons#title","content":"2026-02-226 min lectura\n\nLucio Durán\nEngineering Manager & AI Solutions Architect\n\nTambién disponible en: [English](<https://lucioduran.com/blog/event-sourcing-cqrs-production-lessons>)"},{"heading":"Fundamentos de Event Sourcing","level":2,"url":"https://lucioduran.com/es/blog/event-sourcing-cqrs-production-lessons#fundamentos-de-event-sourcing","content":"En lugar de almacenar el estado actual de una entidad, se almacena la secuencia de eventos que condujeron a ese estado. El estado actual se deriva reproduciendo los eventos. Este patrón se implementa típicamente con [PostgreSQL](<https://www.postgresql.org/>) o un event store dedicado.\n\n```python\n# event_store.py\nfrom dataclasses import dataclass, field\nfrom datetime import datetime\nimport json, uuid\n\n@dataclass(frozen=True)\nclass Event:\n event_id: str\n stream_id: str\n event_type: str\n data: dict\n metadata: dict\n version: int\n timestamp: datetime\n schema_version: int = 1\n\nclass EventStore:\n def __init__(self, connection):\n self.conn = connection\n\n def append(self, stream_id: str, events: list[Event],\n expected_version: int) -> None:\n with self.conn.cursor() as cur:\n cur.execute(\n \"SELECT MAX(version) FROM events WHERE stream_id = %s FOR UPDATE\",\n (stream_id,)\n )\n current_version = cur.fetchone()[0] or 0\n if current_version != expected_version:\n raise ConcurrencyError(\n f\"Versión esperada {expected_version}, \"\n f\"obtuve {current_version} para stream {stream_id}\"\n )\n for i, event in enumerate(events):\n new_version = expected_version + i + 1\n cur.execute(\"\"\"\n INSERT INTO events\n (event_id, stream_id, event_type, data, metadata,\n version, timestamp, schema_version)\n VALUES (%s, %s, %s, %s, %s, %s, %s, %s)\n \"\"\", (\n event.event_id, stream_id, event.event_type,\n json.dumps(event.data), json.dumps(event.metadata),\n new_version, event.timestamp, event.schema_version\n ))\n self.conn.commit()\n```"},{"heading":"El Schema del Event Store","level":3,"url":"https://lucioduran.com/es/blog/event-sourcing-cqrs-production-lessons#el-schema-del-event-store","content":"```sql\nCREATE TABLE events (\n event_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),\n stream_id TEXT NOT NULL,\n event_type TEXT NOT NULL,\n data JSONB NOT NULL,\n metadata JSONB NOT NULL DEFAULT '{}',\n version INTEGER NOT NULL,\n timestamp TIMESTAMPTZ NOT NULL DEFAULT now(),\n schema_version INTEGER NOT NULL DEFAULT 1,\n UNIQUE (stream_id, version)\n);\n\nCREATE INDEX idx_events_stream ON events (stream_id, version);\nCREATE INDEX idx_events_type ON events (event_type, timestamp);\n\nCREATE TABLE snapshots (\n stream_id TEXT NOT NULL,\n version INTEGER NOT NULL,\n state JSONB NOT NULL,\n created_at TIMESTAMPTZ NOT NULL DEFAULT now(),\n PRIMARY KEY (stream_id, version)\n);\n```"},{"heading":"Agregados: Donde Vive la Lógica de Negocio","level":2,"url":"https://lucioduran.com/es/blog/event-sourcing-cqrs-production-lessons#agregados-donde-vive-la-lgica-de-negocio","content":"```python\n# order_aggregate.py\nclass OrderAggregate:\n def __init__(self):\n self.order_id = None\n self.status = None\n self.items = []\n self.total = 0\n self.version = 0\n\n def place_order(self, order_id, items, customer_id):\n if self.status is not None:\n raise InvalidOperationError(\"La orden ya existe\")\n if not items:\n raise ValidationError(\"La orden debe tener al menos un item\")\n total = sum(item['price'] * item['quantity'] for item in items)\n return [Event(\n event_id=str(uuid.uuid4()),\n stream_id=f\"order-{order_id}\",\n event_type=\"OrderPlaced\",\n data={'order_id': order_id, 'customer_id': customer_id,\n 'items': items, 'total': total},\n metadata={'causation_id': str(uuid.uuid4())},\n version=self.version + 1,\n timestamp=datetime.utcnow()\n )]\n\n def apply(self, event: Event):\n handler = getattr(self, f'_on_{event.event_type}', None)\n if handler:\n handler(event.data)\n self.version = event.version\n\n def _on_OrderPlaced(self, data):\n self.order_id = data['order_id']\n self.status = 'placed'\n self.items = data['items']\n self.total = data['total']\n\n def _on_PaymentConfirmed(self, data):\n self.status = 'paid'\n\n def _on_OrderCancelled(self, data):\n self.status = 'cancelled'\n\n @classmethod\n def from_events(cls, events):\n aggregate = cls()\n for event in events:\n aggregate.apply(event)\n return aggregate\n```"},{"heading":"Proyecciones: Construyendo Read Models","level":2,"url":"https://lucioduran.com/es/blog/event-sourcing-cqrs-production-lessons#proyecciones-construyendo-read-models","content":"Las proyecciones transforman el event log en read models optimizados para queries. Esta es la \"Q\" en CQRS.\n\n```python\nclass OrderDashboardProjection:\n def __init__(self, read_db):\n self.db = read_db\n\n def handle(self, event: Event):\n handler = getattr(self, f'_handle_{event.event_type}', None)\n if handler:\n handler(event)\n\n def _handle_OrderPlaced(self, event):\n data = event.data\n self.db.execute(\"\"\"\n INSERT INTO order_dashboard\n (order_id, customer_id, status, total, item_count, placed_at)\n VALUES (%s, %s, 'placed', %s, %s, %s)\n ON CONFLICT (order_id) DO UPDATE SET\n status = 'placed', total = EXCLUDED.total\n \"\"\", (data['order_id'], data['customer_id'],\n data['total'], len(data['items']), event.timestamp))\n\n def rebuild(self, event_store):\n \"\"\"Reconstruir toda la proyección desde cero.\"\"\"\n self.db.execute(\"TRUNCATE order_dashboard\")\n for batch in event_store.read_all(batch_size=1000):\n for event in batch:\n self.handle(event)\n self.db.commit()\n```"},{"heading":"Snapshots: Domando el Costo de Rehidratación","level":2,"url":"https://lucioduran.com/es/blog/event-sourcing-cqrs-production-lessons#snapshots-domando-el-costo-de-rehidratacin","content":"```python\nclass SnapshotRepository:\n def __init__(self, event_store, snapshot_store, interval=100):\n self.event_store = event_store\n self.snapshot_store = snapshot_store\n self.interval = interval\n\n def load(self, stream_id):\n snapshot = self.snapshot_store.get_latest(stream_id)\n if snapshot:\n aggregate = OrderAggregate()\n aggregate.__dict__.update(snapshot['state'])\n aggregate.version = snapshot['version']\n events = self.event_store.read_stream(stream_id, from_version=snapshot['version'])\n else:\n aggregate = OrderAggregate()\n events = self.event_store.read_stream(stream_id)\n\n for event in events:\n aggregate.apply(event)\n return aggregate\n\n def save(self, stream_id, aggregate, new_events):\n self.event_store.append(stream_id, new_events, aggregate.version - len(new_events))\n if aggregate.version % self.interval == 0:\n self.snapshot_store.save(\n stream_id=stream_id, version=aggregate.version,\n state=aggregate.__dict__\n )\n```"},{"heading":"Cuándo NO Usar Event Sourcing","level":2,"url":"https://lucioduran.com/es/blog/event-sourcing-cqrs-production-lessons#cundo-no-usar-event-sourcing","content":"Esta sección aborda los escenarios comunes donde event sourcing empeora las cosas.\n\n**1. Aplicaciones CRUD simples.** Si tu dominio es \"usuario llena formulario, datos van a la base, alguien los lee después,\" event sourcing agrega complejidad sin beneficio. Una tabla PostgreSQL con columnas ` created_at ` y ` updated_at ` cubre el 90% de las necesidades de auditoría.\n\n**2. Cuando tu equipo no entiende DDD.** Event sourcing sin boundaries de agregados correctos se vuelve una pesadilla. Si tu equipo no puede responder \"cuál es el boundary de consistencia aquí?\", se va a terminar con un monolito distribuido que es más difícil de razonar que la base de datos que reemplazó.\n\n**3. Cuando se necesita queries ad-hoc.** Los event stores están optimizados para leer un solo stream. Queries cross-aggregate requieren proyecciones. Si tu caso de uso principal es \"mostrame todas las órdenes del cliente X de marzo que contengan producto Y,\" se necesita una proyección para cada nuevo patrón de query. Con una base relacional, es una cláusula WHERE.\n\n**4. Prototipos y MVPs.** El tiempo que se invierte construyendo infraestructura de event store, rebuild de proyecciones, estrategias de snapshots y upcasting es tiempo que no se está gastando validando tu hipótesis de negocio.\n\n**5. Cuando la consistencia eventual es inaceptable.** Las proyecciones son eventualmente consistentes con el event store. Siempre hay un delay entre que un evento se almacena y el read model se actualiza.\n\nUn ejemplo ilustrativo: un servicio de gestión de usuarios donde los usuarios se registran, actualizan su perfil y cambian su email. Con quizás cinco tipos de eventos, el requerimiento de \"audit trail\" que motivó event sourcing se podría haber resuelto con una simple tabla ` user_audit_log `. En cambio, el sistema tenía un event store, tres proyecciones, una estrategia de snapshots, y un pipeline de upcasting para cuando se agregó un campo ` display_name `. Arquitecturalmente interesante y prácticamente terrible."},{"heading":"Cuándo Event Sourcing Brilla","level":2,"url":"https://lucioduran.com/es/blog/event-sourcing-cqrs-production-lessons#cundo-event-sourcing-brilla","content":"Los sistemas donde event sourcing tiene éxito tienden a compartir rasgos comunes:\n\n- **Lógica de dominio compleja** con muchas transiciones de estado y reglas de negocio\n- **Requerimientos de auditoría** que iban más allá de \"quién cambió qué\" a \"cuál era el estado en el momento T y por qué\"\n- **Integraciones event-driven** donde sistemas downstream necesitaban reaccionar a eventos de dominio\n- **Queries temporales** como \"mostrame la orden como estaba ayer a las 3pm\"\n\nConsidérese una plataforma de trading donde requisitos regulatorios demandan un audit trail completo e inmutable de cada cambio de estado. O un sistema de logística donde docenas de servicios downstream necesitan reaccionar a eventos de envío. En ambos casos, event sourcing es el fit natural, y la alternativa (change data capture sobre una base relacional) sería más compleja, no menos.\n\nLa realidad es que la mayoría de los sistemas no necesitan event sourcing. Pero cuando el caso de uso lo demanda, nada más se acerca."},{"heading":"Preguntas frecuentes","level":2,"url":"https://lucioduran.com/es/blog/event-sourcing-cqrs-production-lessons#questions","content":""},{"heading":"¿Cuándo NO debería usar event sourcing?","level":3,"url":"https://lucioduran.com/es/blog/event-sourcing-cqrs-production-lessons#question-1","content":"Aplicaciones CRUD simples, sistemas donde las pistas de auditoría no son importantes, equipos sin experiencia en DDD, prototipos y MVPs, y cualquier sistema donde la complejidad agregada no se justifica por los beneficios. La mayoría de las aplicaciones se benefician más de una base de datos tradicional con change data capture para necesidades de auditoría."},{"heading":"¿Cuál es la diferencia entre event sourcing y arquitectura event-driven?","level":3,"url":"https://lucioduran.com/es/blog/event-sourcing-cqrs-production-lessons#question-2","content":"La arquitectura event-driven usa eventos para comunicación entre servicios. Event sourcing almacena estado como una secuencia de eventos. Es posible tener arquitectura event-driven sin event sourcing (la mayoría lo hace), y event sourcing sin arquitectura event-driven (ES de servicio único)."},{"heading":"¿Cómo manejar la evolución de esquemas en un event store?","level":3,"url":"https://lucioduran.com/es/blog/event-sourcing-cqrs-production-lessons#question-3","content":"Tres enfoques: upcasting (transformar eventos anteriores al esquema nuevo en lectura), handlers de eventos versionados (manejar v1 y v2 en las proyecciones), o copy-transform (migrar eventos a un store nuevo con el esquema nuevo). Upcasting es el más común y menos disruptivo."},{"heading":"Tips","level":2,"url":"https://lucioduran.com/es/blog/event-sourcing-cqrs-production-lessons#article-tips","content":"1. **Tip:** Almacenar los eventos como hechos inmutables con campos ` schema_version `. Los esquemas de eventos cambiarán, el upcasting de v1 a v2 es mucho más sencillo que reescribir el event store.\n2. **Tip:** Tomar snapshot cada 100-200 eventos por agregado. Sin snapshots, rehidratar un agregado con 10K eventos tarda segundos, los usuarios lo notan.\n3. **Tip:** Las proyecciones deben ser idempotentes y reconstruibles desde cero. Si no es posible eliminar una proyección y reconstruirla desde el event log, la arquitectura tiene una dependencia oculta.\n4. **Tip:** Utilizar ` optimistic concurrency ` en la versión del stream de eventos. Si dos comandos intentan agregar eventos al mismo stream simultáneamente, el segundo debe fallar y reintentar, ese es el límite de consistencia.\n\n- event-sourcing\n- cqrs\n- event-store\n- projections\n- sagas\n- architecture\n- ddd\n\n[Volver al Blog](<https://lucioduran.com/es/blog>)"}],"links":[{"text":"English","url":"https://lucioduran.com/blog/event-sourcing-cqrs-production-lessons"},{"text":"PostgreSQL","url":"https://www.postgresql.org/"},{"text":"Volver al Blog","url":"https://lucioduran.com/es/blog"}],"structuredData":[{"@context":"https://schema.org","@graph":[{"@type":"BlogPosting","@id":"https://lucioduran.com/es/blog/event-sourcing-cqrs-production-lessons#article","mainEntityOfPage":{"@type":"WebPage","@id":"https://lucioduran.com/es/blog/event-sourcing-cqrs-production-lessons"},"headline":"Event Sourcing + CQRS: Diseño de Event Store, Proyecciones, Snapshots y Cuándo NO Usarlo","description":"Lecciones de producción en la construcción de sistemas event-sourced, diseño de event store, rebuilds de proyecciones, estrategias de snapshots, orquestación de sagas, y una evaluación honesta de cuándo event sourcing empeora las cosas.","datePublished":"2026-02-22","author":{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com"},"publisher":{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com"},"image":"https://statics.lucioduran.com/images/lucioduran-social-card.jpg","url":"https://lucioduran.com/es/blog/event-sourcing-cqrs-production-lessons","inLanguage":"es","isPartOf":{"@id":"https://lucioduran.com/blog#blog"},"articleSection":"Architecture","keywords":"event-sourcing, cqrs, event-store, projections, sagas, architecture, ddd","wordCount":1067,"timeRequired":"PT6M","workTranslation":[{"@type":"BlogPosting","inLanguage":"en","url":"https://lucioduran.com/blog/event-sourcing-cqrs-production-lessons"}]},{"@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Inicio","item":"https://lucioduran.com/"},{"@type":"ListItem","position":2,"name":"Blog","item":"https://lucioduran.com/es/blog"},{"@type":"ListItem","position":3,"name":"Event Sourcing + CQRS: Diseño de Event Store, Proyecciones, Snapshots y Cuándo NO Usarlo","item":"https://lucioduran.com/es/blog/event-sourcing-cqrs-production-lessons"}]},{"@type":"FAQPage","mainEntity":[{"@type":"Question","name":"¿Cuándo NO debería usar event sourcing?","acceptedAnswer":{"@type":"Answer","text":"Aplicaciones CRUD simples, sistemas donde las pistas de auditoría no son importantes, equipos sin experiencia en DDD, prototipos y MVPs, y cualquier sistema donde la complejidad agregada no se justifica por los beneficios. La mayoría de las aplicaciones se benefician más de una base de datos tradicional con change data capture para necesidades de auditoría."}},{"@type":"Question","name":"¿Cuál es la diferencia entre event sourcing y arquitectura event-driven?","acceptedAnswer":{"@type":"Answer","text":"La arquitectura event-driven usa eventos para comunicación entre servicios. Event sourcing almacena estado como una secuencia de eventos. Es posible tener arquitectura event-driven sin event sourcing (la mayoría lo hace), y event sourcing sin arquitectura event-driven (ES de servicio único)."}},{"@type":"Question","name":"¿Cómo manejar la evolución de esquemas en un event store?","acceptedAnswer":{"@type":"Answer","text":"Tres enfoques: upcasting (transformar eventos anteriores al esquema nuevo en lectura), handlers de eventos versionados (manejar v1 y v2 en las proyecciones), o copy-transform (migrar eventos a un store nuevo con el esquema nuevo). Upcasting es el más común y menos disruptivo."}}]}]}],"contentHash":"431e6040a67c608db555905a78f54c8a698b6cdb00a7f8605602c6a170385382","provenance":{"method":"Deterministic serialization of the server-rendered main element","source":"https://lucioduran.com/es/blog/event-sourcing-cqrs-production-lessons","hashAlgorithm":"sha256","hashScope":"JSON document fields before contentHash and provenance","sourceHTMLBytes":61061,"contentBytes":11105}}
