{"schemaVersion":1,"id":"https://lucioduran.com/es/blog/temporal-workflow-engine-durable-execution","url":"https://lucioduran.com/es/blog/temporal-workflow-engine-durable-execution","title":"Temporal.io: Reemplazando Colas de Mensajes con Ejecución Durable para Orquestación Compleja","description":"Análisis del modelo de ejecución durable de Temporal, las restricciones de determinismo de workflows, reintentos de activities, patrones saga, y por qué la ejecución durable supera a la orquestación construida sobre colas de mensajes.","language":"es","datePublished":"2026-02-28","dateModified":null,"contentFormat":"text/markdown","content":"Backend\n\n# Temporal.io: Reemplazando Colas de Mensajes con Ejecución Durable para Orquestación Compleja\n\n2026-02-2810 min lectura\n\nLucio Durán\nEngineering Manager & AI Solutions Architect\n\nTambién disponible en: [English](<https://lucioduran.com/blog/temporal-workflow-engine-durable-execution>)\n\n## Definición de Ejecución Durable\n\nLa idea central detrás de [Temporal](<https://temporal.io/>) es simple de enunciar y transformadora en la práctica: **el código se persiste**. No solo los datos, no solo el estado, el progreso real de ejecución de la función se graba para que si el proceso crashea, pueda resumir exactamente donde quedó.\n\nAquí va un workflow de Temporal en TypeScript:\n\n```typescript\nimport { proxyActivities, sleep, ApplicationFailure } from '@temporalio/workflow';\nimport type * as activities from './activities';\n\nconst { chargePayment, reserveInventory, shipOrder, sendConfirmation, refundPayment, releaseInventory } =\n proxyActivities<typeof activities>({\n startToCloseTimeout: '30s',\n retry: {\n maximumAttempts: 3,\n backoffCoefficient: 2,\n initialInterval: '1s',\n },\n });\n\nexport async function processOrder(order: OrderInput): Promise<OrderResult> {\n // Paso 1: Reservar inventario\n const reservation = await reserveInventory(order.items);\n\n // Paso 2: Cobrar pago\n let paymentId: string;\n try {\n paymentId = await chargePayment(order.paymentMethod, order.total);\n } catch (err) {\n // Compensación: liberar inventario si falla el pago\n await releaseInventory(reservation.id);\n throw ApplicationFailure.nonRetryable(\n `Pago falló: ${err.message}`\n );\n }\n\n // Paso 3: Enviar la orden\n let shipmentId: string;\n try {\n shipmentId = await shipOrder(reservation.id, order.shippingAddress);\n } catch (err) {\n // Compensación: reembolsar pago y liberar inventario\n await refundPayment(paymentId);\n await releaseInventory(reservation.id);\n throw ApplicationFailure.nonRetryable(\n `Envío falló: ${err.message}`\n );\n }\n\n // Paso 4: Enviar confirmación\n await sendConfirmation(order.email, {\n orderId: order.id,\n paymentId,\n shipmentId,\n items: order.items,\n });\n\n return { orderId: order.id, paymentId, shipmentId, status: 'completed' };\n}\n```\n\nObservar este código. Es simplemente una función async regular con bloques try-catch. Sin máquinas de estado. Sin serialización de estados intermedios a una base de datos. Sin lógica de reintentos wrapeada alrededor de cada llamada externa. Sin manejo de dead-letter queues.\n\nPero aquí va lo que Temporal hace detrás de escena:\n\n1. Cuando se llama ` reserveInventory `, Temporal graba un evento \"ScheduleActivityTask\" en el historial de eventos del workflow\n2. Cuando la activity completa, se graba un evento \"ActivityTaskCompleted\" con el resultado\n3. Si el proceso del worker crashea entre los pase es 2 y 3 (pago), un nuevo worker levanta el workflow\n4. Temporal **hace replay** de la función del workflow desde el principio, pero en vez de re-ejecutar ` reserveInventory `, lee el resultado grabado del historial de eventos\n5. El replay llega al punto de falla y continúa ejecutando los pase es restantes\n\nEsto es lo que significa \"ejecución durable\". La función del workflow parece correr continuamente, pero en realidad es una serie de pase es grabados que pueden ser replayeados en cualquier worker. El ` await ` en cada activity es un punto de persistencia.\n\n## La Restricción de Determinismo\n\nHay una restricción fundamental que hace que esto funcione: **las funciones de workflow deben ser determinísticas**. Dados los mismos inputs y los mismos resultados de activities, el workflow debe tomar la misma secuencia de decisiones.\n\nEsto significa que no es posible hacer lo siguiente dentro de una función de workflow:\n\n```typescript\n// TODO ESTO VA A ROMPER EL REPLAY\n\n// 1. Tiempo no-determinístico\nconst now = Date.now(); // ¡Diferente en replay!\n// Utilizar en cambio: workflow.now()\n\n// 2. Números random\nconst id = Math.random().toString(36); // ¡Diferente en replay!\n// Utilizar en cambio: workflow.uuid4()\n\n// 3. I/O directo\nconst data = await fetch('https://api.example.com/data'); // ¡Efecto secundario!\n// Utilizar en cambio: una activity\n\n// 4. Iteración no-determinística\nconst keys = Object.keys(someMap); // ¡Orden no garantizado!\n// Utilizar en cambio: keys ordenadas o arrays\n```\n\nEsta restricción inicialmente se siente limitante. Con el uso extendido, se revela como un feature. Fuerza una separación limpia entre **decisiones** (workflow) y **efectos** (activities). El workflow es lógica pura. Las activities son donde pasa toda la interacción sucia con el mundo real.\n\nLas activities son funciones regulares que pueden hacer cualquier cosa, llamadas HTTP, queries a bases de datos, I/O de archivos, llamadas a APIs de terceros. Corren en workers, y Temporal maneja reintentos, timeouts y heartbeating:\n\n```typescript\n// activities.ts, estas corren en workers, NO en el sandbox del workflow\nimport { Context } from '@temporalio/activity';\n\nexport async function chargePayment(\n method: PaymentMethod,\n amount: number\n): Promise<string> {\n // Heartbeat para operaciones largas, le avisa a Temporal que estamos vivos\n Context.current().heartbeat('cobrando');\n\n const result = await stripe.charges.create({\n amount: Math.round(amount * 100),\n currency: 'usd',\n source: method.token,\n });\n\n return result.id;\n}\n\nexport async function shipOrder(\n reservationId: string,\n address: ShippingAddress\n): Promise<string> {\n Context.current().heartbeat('creando envío');\n\n const shipment = await shippingProvider.createShipment({\n reservationId,\n address,\n service: 'standard',\n });\n\n return shipment.trackingNumber;\n}\n```\n\n## Patrón Saga: Compensación Hecha Bien\n\nEl workflow de procesamiento de órdenes de arriba implementa el patrón saga, una secuencia de transacciones con acciones compensatorias si algún paso falla. En un sistema típico basado en RabbitMQ, el coordinador de sagas es su propio servicio con su propia base de datos, trackeando el estado de cada instancia de saga.\n\nCon Temporal, el patrón saga son simplemente bloques try-catch. Pero para workflows más complejos con muchos pasos, un helper que acumula compensaciones es útil:\n\n```typescript\nclass SagaCompensation {\n private compensations: Array<() => Promise<void>> = [];\n\n addCompensation(fn: () => Promise<void>): void {\n this.compensations.unshift(fn); // LIFO, compensar en orden inverso\n }\n\n async compensate(): Promise<void> {\n for (const compensation of this.compensations) {\n try {\n await compensation();\n } catch (err) {\n // Loguear pero continuar, compensación parcial es mejor que nada\n console.error('Compensación falló:', err);\n }\n }\n }\n}\n\nexport async function processComplexOrder(order: ComplexOrder): Promise<void> {\n const saga = new SagaCompensation();\n\n try {\n const reservation = await reserveInventory(order.items);\n saga.addCompensation(() => releaseInventory(reservation.id));\n\n const paymentId = await chargePayment(order.payment, order.total);\n saga.addCompensation(() => refundPayment(paymentId));\n\n const discountApplied = await applyLoyaltyDiscount(order.customerId, order.total);\n saga.addCompensation(() => revertLoyaltyDiscount(order.customerId, discountApplied));\n\n const shipmentId = await shipOrder(reservation.id, order.address);\n saga.addCompensation(() => cancelShipment(shipmentId));\n\n await updateOrderStatus(order.id, 'completed');\n await sendConfirmation(order.email, { reservation, paymentId, shipmentId });\n\n } catch (err) {\n await saga.compensate();\n await updateOrderStatus(order.id, 'failed');\n throw err;\n }\n}\n```\n\nEste es el mismo patrón que un coordinador de sagas de 3.000 líneas implementa, en unas 50 líneas. Y es confiable, si un worker crashea durante la compensación, Temporal hace replay del workflow y la compensación continúa desde donde quedó.\n\n## Señales, Queries, y Human-in-the-Loop\n\nLos workflows de Temporal pueden recibir señales externas (eventos que modifican el estado del workflow) y responder a queries (inspección read-only del estado del workflow). Esto es increíblemente poderoso para workflows que necesitan aprobación humana o input externo:\n\n```typescript\nimport { defineSignal, defineQuery, setHandler, condition } from '@temporalio/workflow';\n\nconst approveSignal = defineSignal<[{ approvedBy: string; notes: string }]>('approve');\nconst rejectSignal = defineSignal<[{ rejectedBy: string; reason: string }]>('reject');\nconst statusQuery = defineQuery<OrderStatus>('status');\n\nexport async function orderWithApproval(order: OrderInput): Promise<OrderResult> {\n let status: OrderStatus = 'pending_approval';\n let approval: { approvedBy: string; notes: string } | null = null;\n let rejection: { rejectedBy: string; reason: string } | null = null;\n\n setHandler(approveSignal, (data) => {\n approval = data;\n status = 'approved';\n });\n\n setHandler(rejectSignal, (data) => {\n rejection = data;\n status = 'rejected';\n });\n\n setHandler(statusQuery, () => status);\n\n // Esperar aprobación o rechazo, con timeout\n const approved = await condition(\n () => approval !== null || rejection !== null,\n '24h' // Timeout después de 24 horas\n );\n\n if (!approved || rejection) {\n status = 'rejected';\n await notifyRejection(order.email, rejection?.reason ?? 'Timeout de aprobación');\n return { orderId: order.id, status: 'rejected' };\n }\n\n status = 'processing';\n // ... resto del workflow de la orden\n}\n```\n\nDesde afuera, le se envía una señal al workflow:\n\n```typescript\nconst handle = client.workflow.getHandle('order-12345');\nawait handle.signal(approveSignal, {\n approvedBy: 'manager@company.com',\n notes: 'Aprobado para cliente VIP',\n});\n```\n\nEl workflow puede sentarse esperando aprobación por horas, días o semanas. El worker no mantiene una conexión abierta. No pollea. El estado del workflow está persistido en la base de datos de Temporal. Cuando la señal llega, Temporal agenda el workflow para ejecución, un worker lo levanta, hace replay del historial, procesa la señal, y continúa.\n\n## Arquitectura de Producción\n\nUn setup de producción corre Temporal Server en Kubernetes con un backend PostgreSQL:\n\n```yaml\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: temporal-server\nspec:\n replicas: 3\n template:\n spec:\n containers:\n - name: temporal-server\n image: temporalio/server:1.25.2\n env:\n - name: DB\n value: postgresql\n - name: POSTGRES_HOST\n value: temporal-postgres.db.svc.cluster.local\n - name: NUM_HISTORY_SHARDS\n value: \"512\"\n ports:\n - containerPort: 7233\n - containerPort: 7234\n - containerPort: 7235\n - containerPort: 7239\n```\n\nLos workers corren como deployments separados:\n\n```typescript\n// worker.ts\nimport { Worker } from '@temporalio/worker';\nimport * as activities from './activities';\n\nasync function run() {\n const worker = await Worker.create({\n workflowsPath: require.resolve('./workflows'),\n activities,\n taskQueue: 'order-processing',\n maxConcurrentActivityTaskExecutions: 100,\n maxConcurrentWorkflowTaskExecutions: 50,\n stickyQueueScheduleToStartTimeout: '10s',\n });\n\n await worker.run();\n}\n\nrun().catch((err) => {\n console.error(err);\n process.exit(1);\n});\n```\n\n## Componentes Reemplazados y Resultados Medidos\n\nAntes de Temporal:\n\n- 7 colas de RabbitMQ, 4 dead-letter queues\n- 12 servicios consumidores (Node.js)\n- 1 servicio coordinador de sagas (3.000 líneas)\n- 1 base de datos PostgreSQL para estado de sagas\n- Lógica de reintentos custom en cada consumidor\n- Scripts de compensación manuales para sagas trabadas\n\nDespués de Temporal:\n\n- 1 cluster de Temporal (3 nodos)\n- 3 deployments de workers (orden, pago, envío)\n- ~800 líneas de código de workflow\n- ~1.200 líneas de código de activities\n- Cero lógica de reintentos custom\n- Cero scripts de compensación manuales\n\nEl impacto operacional:\n\n- **Sagas trabadas/huérfanas**: Pasaron de ~15/semana a cero. Temporal no pierde el rastro de los workflows.\n- **Incidentes de doble cobro**: Pasaron de ~3/mes a cero. El patrón saga con ejecución durable realmente garantiza que la compensación se ejecute.\n- **Tiempo medio para diagnosticar problemas**: Bajó de 2-3 horas (correlacionar logs entre 12 servicios) a 5-10 minutos (mirar el historial de eventos del workflow en la UI de Temporal).\n- **Alertas de guardia**: Bajaron un 70%. La mayoría de las alertas viejas eran por consumidores trabados o backlogs en las colas.\n\nEl historial de eventos en la UI de Temporal es genuinamente transformador para debugging. Cada llamada a activity, cada resultado, cada reintento, cada señal, está todo ahí en orden cronológico. Cuando un cliente reporta un problema, el workflow se puede buscar por ID de orden, mostrando exactamente qué pasó, cuándo, y por qué.\n\n## Cuándo No Usar Temporal\n\nTemporal no es la herramienta correcta para todo:\n\n**APIs simples de request-response**: Si tu endpoint llama a una base de datos y devuelve un resultado, no se necesita un workflow engine. El overhead de grabar eventos y hacer replay del historial es desperdiciado.\n\n**Operaciones de alta frecuencia y baja latencia**: Temporal agrega ~20-50ms de overhead por workflow task. Para operaciones que necesitan latencia sub-milisegundo, utilizar llamadas directas entre servicios.\n\n**Event streaming puro**: Si se necesita broadcastear eventos a muchos consumidores (fan-out), Kafka es mejor. Temporal es para orquestar una secuencia específica de pasos, no para broadcastear.\n\n**Workflows con payloads enormes**: Temporal almacena los inputs y outputs de activities en su historial de eventos. Si tus activities pasan blobs de 10MB, tu historial va a ser enorme. Pasar referencias (keys de S3, IDs de base de datos) en vez de datos.\n\nEl punto óptimo es cualquier proceso multi-paso que abarca múltiples servicios, necesita reintentos y compensación, y donde la correctitud importa más que el throughput crudo. Procesamiento de órdenes, flujos de pago, secuencias de onboarding de usuarios, orquestación de pipelines de datos, workflows de aprobación, ahí es donde Temporal brilla.\n\nPara un pipeline típico de procesamiento de órdenes, la migración toma unas seis semanas incluyendo testing. Típicamente se pasa más tiempo descomisionando la infraestructura vieja que construyendo los nuevos workflows. Eso revela dónde realmente vive la complejidad.\n\n## Preguntas frecuentes\n\n### Cómo se compara Temporal con AWS Step Functions?\n\nStep Functions usa una definición de state machine basada en JSON (ASL), mientras que Temporal te deja escribir workflows como código regular en Go, TypeScript, Java o Python. Temporal te da más flexibilidad para lógica compleja (loops, condicionales, sub-workflows) y no tiene el límite de 25K eventos en el historial. Step Functions es más fácil para workflows lineales simples y tiene integración más ajustada con AWS.\n\n### Temporal puede reemplazar a Kafka para arquitecturas event-driven?\n\nNo directamente. Temporal es para orquestación, coordinar una secuencia de pase es con reintentos y compensación. Kafka es para event streaming, broadcastear eventos a múltiples consumidores independientes. Son complementarios. Temporal puede orquestar workflows que se disparan por eventos de Kafka, combinando las fortalezas de ambos.\n\n### Qué pasa con los workflows corriendo cuando deployeo código nuevo?\n\nTemporal hace replay del historial de eventos del workflow contra tu código nuevo. Si tu código nuevo es deterministic-compatible con el viejo (misma secuencia de llamadas a activities, timers, etc.), el replay tiene éxito y el workflow continúa. Si cambiaste la secuencia, el replay falla con un error de no-determinismo. Utilizar workflow.patched() o versionado de task queues para manejar esto.\n\n## Tips\n\n1. **Tip:** No invocar a ` Date.now() ` o ` Math.random() ` dentro de una función de workflow de Temporal. Utilizar ` workflow.now() ` y el random determinístico de ` @temporalio/workflow `. Los replays van a divergir silenciosamente, y te se va a enterar cuando un workflow falle en recuperarse después de un restart del worker, generalmente a las 3 de la mañana.\n2. **Tip:** Configurar ` startToCloseTimeout ` y ` scheduleToCloseTimeout ` en cada activity. Las activities pueden colgarse para siempre en llamadas a APIs externas sin timeout, lo que significa que el workflow también espera para siempre. Establecer 30 segundos por defecto para llamadas a API, 5 minutos para operaciones batch, y siempre agregar un ` heartbeatTimeout ` para activities de larga duración.\n3. **Tip:** Utilizar ` workflow.patched() ` para versionado en vez de intentar deployar cambios breaking en workflows. Los workflows corriendo van a hacer replay con el code path viejo mientras los nuevos usan la versión parcheada. Un intento de migración big-bang causó que 400 workflows in-flight fallaran todos simultáneamente un lunes a la mañana.\n4. **Tip:** Ejecutar el CLI ` tctl ` de Temporal con ` workflow describe -w <id> ` antes de debuggear cualquier cosa en tu código. La mitad de las veces el problema es una retry policy mal configurada o un timeout demasiado agresivo, no tu lógica de negocio.\n\n- temporal\n- durable-execution\n- workflow-engine\n- saga-pattern\n- orchestration\n- distributed-systems\n- message-queues\n- backend-architecture\n\n[Volver al Blog](<https://lucioduran.com/es/blog>)","headings":[{"level":1,"text":"Temporal.io: Reemplazando Colas de Mensajes con Ejecución Durable para Orquestación Compleja","url":"https://lucioduran.com/es/blog/temporal-workflow-engine-durable-execution#title"},{"level":2,"text":"Definición de Ejecución Durable","url":"https://lucioduran.com/es/blog/temporal-workflow-engine-durable-execution#definicin-de-ejecucin-durable"},{"level":2,"text":"La Restricción de Determinismo","url":"https://lucioduran.com/es/blog/temporal-workflow-engine-durable-execution#la-restriccin-de-determinismo"},{"level":2,"text":"Patrón Saga: Compensación Hecha Bien","url":"https://lucioduran.com/es/blog/temporal-workflow-engine-durable-execution#patrn-saga-compensacin-hecha-bien"},{"level":2,"text":"Señales, Queries, y Human-in-the-Loop","url":"https://lucioduran.com/es/blog/temporal-workflow-engine-durable-execution#seales-queries-y-human-in-the-loop"},{"level":2,"text":"Arquitectura de Producción","url":"https://lucioduran.com/es/blog/temporal-workflow-engine-durable-execution#arquitectura-de-produccin"},{"level":2,"text":"Componentes Reemplazados y Resultados Medidos","url":"https://lucioduran.com/es/blog/temporal-workflow-engine-durable-execution#componentes-reemplazados-y-resultados-medidos"},{"level":2,"text":"Cuándo No Usar Temporal","url":"https://lucioduran.com/es/blog/temporal-workflow-engine-durable-execution#cundo-no-usar-temporal"},{"level":2,"text":"Preguntas frecuentes","url":"https://lucioduran.com/es/blog/temporal-workflow-engine-durable-execution#questions"},{"level":3,"text":"Cómo se compara Temporal con AWS Step Functions?","url":"https://lucioduran.com/es/blog/temporal-workflow-engine-durable-execution#question-1"},{"level":3,"text":"Temporal puede reemplazar a Kafka para arquitecturas event-driven?","url":"https://lucioduran.com/es/blog/temporal-workflow-engine-durable-execution#question-2"},{"level":3,"text":"Qué pasa con los workflows corriendo cuando deployeo código nuevo?","url":"https://lucioduran.com/es/blog/temporal-workflow-engine-durable-execution#question-3"},{"level":2,"text":"Tips","url":"https://lucioduran.com/es/blog/temporal-workflow-engine-durable-execution#article-tips"}],"sections":[{"heading":"Temporal.io: Reemplazando Colas de Mensajes con Ejecución Durable para Orquestación Compleja","level":1,"url":"https://lucioduran.com/es/blog/temporal-workflow-engine-durable-execution","content":"Backend"},{"heading":"Temporal.io: Reemplazando Colas de Mensajes con Ejecución Durable para Orquestación Compleja","level":1,"url":"https://lucioduran.com/es/blog/temporal-workflow-engine-durable-execution#title","content":"2026-02-2810 min lectura\n\nLucio Durán\nEngineering Manager & AI Solutions Architect\n\nTambién disponible en: [English](<https://lucioduran.com/blog/temporal-workflow-engine-durable-execution>)"},{"heading":"Definición de Ejecución Durable","level":2,"url":"https://lucioduran.com/es/blog/temporal-workflow-engine-durable-execution#definicin-de-ejecucin-durable","content":"La idea central detrás de [Temporal](<https://temporal.io/>) es simple de enunciar y transformadora en la práctica: **el código se persiste**. No solo los datos, no solo el estado, el progreso real de ejecución de la función se graba para que si el proceso crashea, pueda resumir exactamente donde quedó.\n\nAquí va un workflow de Temporal en TypeScript:\n\n```typescript\nimport { proxyActivities, sleep, ApplicationFailure } from '@temporalio/workflow';\nimport type * as activities from './activities';\n\nconst { chargePayment, reserveInventory, shipOrder, sendConfirmation, refundPayment, releaseInventory } =\n proxyActivities<typeof activities>({\n startToCloseTimeout: '30s',\n retry: {\n maximumAttempts: 3,\n backoffCoefficient: 2,\n initialInterval: '1s',\n },\n });\n\nexport async function processOrder(order: OrderInput): Promise<OrderResult> {\n // Paso 1: Reservar inventario\n const reservation = await reserveInventory(order.items);\n\n // Paso 2: Cobrar pago\n let paymentId: string;\n try {\n paymentId = await chargePayment(order.paymentMethod, order.total);\n } catch (err) {\n // Compensación: liberar inventario si falla el pago\n await releaseInventory(reservation.id);\n throw ApplicationFailure.nonRetryable(\n `Pago falló: ${err.message}`\n );\n }\n\n // Paso 3: Enviar la orden\n let shipmentId: string;\n try {\n shipmentId = await shipOrder(reservation.id, order.shippingAddress);\n } catch (err) {\n // Compensación: reembolsar pago y liberar inventario\n await refundPayment(paymentId);\n await releaseInventory(reservation.id);\n throw ApplicationFailure.nonRetryable(\n `Envío falló: ${err.message}`\n );\n }\n\n // Paso 4: Enviar confirmación\n await sendConfirmation(order.email, {\n orderId: order.id,\n paymentId,\n shipmentId,\n items: order.items,\n });\n\n return { orderId: order.id, paymentId, shipmentId, status: 'completed' };\n}\n```\n\nObservar este código. Es simplemente una función async regular con bloques try-catch. Sin máquinas de estado. Sin serialización de estados intermedios a una base de datos. Sin lógica de reintentos wrapeada alrededor de cada llamada externa. Sin manejo de dead-letter queues.\n\nPero aquí va lo que Temporal hace detrás de escena:\n\n1. Cuando se llama ` reserveInventory `, Temporal graba un evento \"ScheduleActivityTask\" en el historial de eventos del workflow\n2. Cuando la activity completa, se graba un evento \"ActivityTaskCompleted\" con el resultado\n3. Si el proceso del worker crashea entre los pase es 2 y 3 (pago), un nuevo worker levanta el workflow\n4. Temporal **hace replay** de la función del workflow desde el principio, pero en vez de re-ejecutar ` reserveInventory `, lee el resultado grabado del historial de eventos\n5. El replay llega al punto de falla y continúa ejecutando los pase es restantes\n\nEsto es lo que significa \"ejecución durable\". La función del workflow parece correr continuamente, pero en realidad es una serie de pase es grabados que pueden ser replayeados en cualquier worker. El ` await ` en cada activity es un punto de persistencia."},{"heading":"La Restricción de Determinismo","level":2,"url":"https://lucioduran.com/es/blog/temporal-workflow-engine-durable-execution#la-restriccin-de-determinismo","content":"Hay una restricción fundamental que hace que esto funcione: **las funciones de workflow deben ser determinísticas**. Dados los mismos inputs y los mismos resultados de activities, el workflow debe tomar la misma secuencia de decisiones.\n\nEsto significa que no es posible hacer lo siguiente dentro de una función de workflow:\n\n```typescript\n// TODO ESTO VA A ROMPER EL REPLAY\n\n// 1. Tiempo no-determinístico\nconst now = Date.now(); // ¡Diferente en replay!\n// Utilizar en cambio: workflow.now()\n\n// 2. Números random\nconst id = Math.random().toString(36); // ¡Diferente en replay!\n// Utilizar en cambio: workflow.uuid4()\n\n// 3. I/O directo\nconst data = await fetch('https://api.example.com/data'); // ¡Efecto secundario!\n// Utilizar en cambio: una activity\n\n// 4. Iteración no-determinística\nconst keys = Object.keys(someMap); // ¡Orden no garantizado!\n// Utilizar en cambio: keys ordenadas o arrays\n```\n\nEsta restricción inicialmente se siente limitante. Con el uso extendido, se revela como un feature. Fuerza una separación limpia entre **decisiones** (workflow) y **efectos** (activities). El workflow es lógica pura. Las activities son donde pasa toda la interacción sucia con el mundo real.\n\nLas activities son funciones regulares que pueden hacer cualquier cosa, llamadas HTTP, queries a bases de datos, I/O de archivos, llamadas a APIs de terceros. Corren en workers, y Temporal maneja reintentos, timeouts y heartbeating:\n\n```typescript\n// activities.ts, estas corren en workers, NO en el sandbox del workflow\nimport { Context } from '@temporalio/activity';\n\nexport async function chargePayment(\n method: PaymentMethod,\n amount: number\n): Promise<string> {\n // Heartbeat para operaciones largas, le avisa a Temporal que estamos vivos\n Context.current().heartbeat('cobrando');\n\n const result = await stripe.charges.create({\n amount: Math.round(amount * 100),\n currency: 'usd',\n source: method.token,\n });\n\n return result.id;\n}\n\nexport async function shipOrder(\n reservationId: string,\n address: ShippingAddress\n): Promise<string> {\n Context.current().heartbeat('creando envío');\n\n const shipment = await shippingProvider.createShipment({\n reservationId,\n address,\n service: 'standard',\n });\n\n return shipment.trackingNumber;\n}\n```"},{"heading":"Patrón Saga: Compensación Hecha Bien","level":2,"url":"https://lucioduran.com/es/blog/temporal-workflow-engine-durable-execution#patrn-saga-compensacin-hecha-bien","content":"El workflow de procesamiento de órdenes de arriba implementa el patrón saga, una secuencia de transacciones con acciones compensatorias si algún paso falla. En un sistema típico basado en RabbitMQ, el coordinador de sagas es su propio servicio con su propia base de datos, trackeando el estado de cada instancia de saga.\n\nCon Temporal, el patrón saga son simplemente bloques try-catch. Pero para workflows más complejos con muchos pasos, un helper que acumula compensaciones es útil:\n\n```typescript\nclass SagaCompensation {\n private compensations: Array<() => Promise<void>> = [];\n\n addCompensation(fn: () => Promise<void>): void {\n this.compensations.unshift(fn); // LIFO, compensar en orden inverso\n }\n\n async compensate(): Promise<void> {\n for (const compensation of this.compensations) {\n try {\n await compensation();\n } catch (err) {\n // Loguear pero continuar, compensación parcial es mejor que nada\n console.error('Compensación falló:', err);\n }\n }\n }\n}\n\nexport async function processComplexOrder(order: ComplexOrder): Promise<void> {\n const saga = new SagaCompensation();\n\n try {\n const reservation = await reserveInventory(order.items);\n saga.addCompensation(() => releaseInventory(reservation.id));\n\n const paymentId = await chargePayment(order.payment, order.total);\n saga.addCompensation(() => refundPayment(paymentId));\n\n const discountApplied = await applyLoyaltyDiscount(order.customerId, order.total);\n saga.addCompensation(() => revertLoyaltyDiscount(order.customerId, discountApplied));\n\n const shipmentId = await shipOrder(reservation.id, order.address);\n saga.addCompensation(() => cancelShipment(shipmentId));\n\n await updateOrderStatus(order.id, 'completed');\n await sendConfirmation(order.email, { reservation, paymentId, shipmentId });\n\n } catch (err) {\n await saga.compensate();\n await updateOrderStatus(order.id, 'failed');\n throw err;\n }\n}\n```\n\nEste es el mismo patrón que un coordinador de sagas de 3.000 líneas implementa, en unas 50 líneas. Y es confiable, si un worker crashea durante la compensación, Temporal hace replay del workflow y la compensación continúa desde donde quedó."},{"heading":"Señales, Queries, y Human-in-the-Loop","level":2,"url":"https://lucioduran.com/es/blog/temporal-workflow-engine-durable-execution#seales-queries-y-human-in-the-loop","content":"Los workflows de Temporal pueden recibir señales externas (eventos que modifican el estado del workflow) y responder a queries (inspección read-only del estado del workflow). Esto es increíblemente poderoso para workflows que necesitan aprobación humana o input externo:\n\n```typescript\nimport { defineSignal, defineQuery, setHandler, condition } from '@temporalio/workflow';\n\nconst approveSignal = defineSignal<[{ approvedBy: string; notes: string }]>('approve');\nconst rejectSignal = defineSignal<[{ rejectedBy: string; reason: string }]>('reject');\nconst statusQuery = defineQuery<OrderStatus>('status');\n\nexport async function orderWithApproval(order: OrderInput): Promise<OrderResult> {\n let status: OrderStatus = 'pending_approval';\n let approval: { approvedBy: string; notes: string } | null = null;\n let rejection: { rejectedBy: string; reason: string } | null = null;\n\n setHandler(approveSignal, (data) => {\n approval = data;\n status = 'approved';\n });\n\n setHandler(rejectSignal, (data) => {\n rejection = data;\n status = 'rejected';\n });\n\n setHandler(statusQuery, () => status);\n\n // Esperar aprobación o rechazo, con timeout\n const approved = await condition(\n () => approval !== null || rejection !== null,\n '24h' // Timeout después de 24 horas\n );\n\n if (!approved || rejection) {\n status = 'rejected';\n await notifyRejection(order.email, rejection?.reason ?? 'Timeout de aprobación');\n return { orderId: order.id, status: 'rejected' };\n }\n\n status = 'processing';\n // ... resto del workflow de la orden\n}\n```\n\nDesde afuera, le se envía una señal al workflow:\n\n```typescript\nconst handle = client.workflow.getHandle('order-12345');\nawait handle.signal(approveSignal, {\n approvedBy: 'manager@company.com',\n notes: 'Aprobado para cliente VIP',\n});\n```\n\nEl workflow puede sentarse esperando aprobación por horas, días o semanas. El worker no mantiene una conexión abierta. No pollea. El estado del workflow está persistido en la base de datos de Temporal. Cuando la señal llega, Temporal agenda el workflow para ejecución, un worker lo levanta, hace replay del historial, procesa la señal, y continúa."},{"heading":"Arquitectura de Producción","level":2,"url":"https://lucioduran.com/es/blog/temporal-workflow-engine-durable-execution#arquitectura-de-produccin","content":"Un setup de producción corre Temporal Server en Kubernetes con un backend PostgreSQL:\n\n```yaml\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: temporal-server\nspec:\n replicas: 3\n template:\n spec:\n containers:\n - name: temporal-server\n image: temporalio/server:1.25.2\n env:\n - name: DB\n value: postgresql\n - name: POSTGRES_HOST\n value: temporal-postgres.db.svc.cluster.local\n - name: NUM_HISTORY_SHARDS\n value: \"512\"\n ports:\n - containerPort: 7233\n - containerPort: 7234\n - containerPort: 7235\n - containerPort: 7239\n```\n\nLos workers corren como deployments separados:\n\n```typescript\n// worker.ts\nimport { Worker } from '@temporalio/worker';\nimport * as activities from './activities';\n\nasync function run() {\n const worker = await Worker.create({\n workflowsPath: require.resolve('./workflows'),\n activities,\n taskQueue: 'order-processing',\n maxConcurrentActivityTaskExecutions: 100,\n maxConcurrentWorkflowTaskExecutions: 50,\n stickyQueueScheduleToStartTimeout: '10s',\n });\n\n await worker.run();\n}\n\nrun().catch((err) => {\n console.error(err);\n process.exit(1);\n});\n```"},{"heading":"Componentes Reemplazados y Resultados Medidos","level":2,"url":"https://lucioduran.com/es/blog/temporal-workflow-engine-durable-execution#componentes-reemplazados-y-resultados-medidos","content":"Antes de Temporal:\n\n- 7 colas de RabbitMQ, 4 dead-letter queues\n- 12 servicios consumidores (Node.js)\n- 1 servicio coordinador de sagas (3.000 líneas)\n- 1 base de datos PostgreSQL para estado de sagas\n- Lógica de reintentos custom en cada consumidor\n- Scripts de compensación manuales para sagas trabadas\n\nDespués de Temporal:\n\n- 1 cluster de Temporal (3 nodos)\n- 3 deployments de workers (orden, pago, envío)\n- ~800 líneas de código de workflow\n- ~1.200 líneas de código de activities\n- Cero lógica de reintentos custom\n- Cero scripts de compensación manuales\n\nEl impacto operacional:\n\n- **Sagas trabadas/huérfanas**: Pasaron de ~15/semana a cero. Temporal no pierde el rastro de los workflows.\n- **Incidentes de doble cobro**: Pasaron de ~3/mes a cero. El patrón saga con ejecución durable realmente garantiza que la compensación se ejecute.\n- **Tiempo medio para diagnosticar problemas**: Bajó de 2-3 horas (correlacionar logs entre 12 servicios) a 5-10 minutos (mirar el historial de eventos del workflow en la UI de Temporal).\n- **Alertas de guardia**: Bajaron un 70%. La mayoría de las alertas viejas eran por consumidores trabados o backlogs en las colas.\n\nEl historial de eventos en la UI de Temporal es genuinamente transformador para debugging. Cada llamada a activity, cada resultado, cada reintento, cada señal, está todo ahí en orden cronológico. Cuando un cliente reporta un problema, el workflow se puede buscar por ID de orden, mostrando exactamente qué pasó, cuándo, y por qué."},{"heading":"Cuándo No Usar Temporal","level":2,"url":"https://lucioduran.com/es/blog/temporal-workflow-engine-durable-execution#cundo-no-usar-temporal","content":"Temporal no es la herramienta correcta para todo:\n\n**APIs simples de request-response**: Si tu endpoint llama a una base de datos y devuelve un resultado, no se necesita un workflow engine. El overhead de grabar eventos y hacer replay del historial es desperdiciado.\n\n**Operaciones de alta frecuencia y baja latencia**: Temporal agrega ~20-50ms de overhead por workflow task. Para operaciones que necesitan latencia sub-milisegundo, utilizar llamadas directas entre servicios.\n\n**Event streaming puro**: Si se necesita broadcastear eventos a muchos consumidores (fan-out), Kafka es mejor. Temporal es para orquestar una secuencia específica de pasos, no para broadcastear.\n\n**Workflows con payloads enormes**: Temporal almacena los inputs y outputs de activities en su historial de eventos. Si tus activities pasan blobs de 10MB, tu historial va a ser enorme. Pasar referencias (keys de S3, IDs de base de datos) en vez de datos.\n\nEl punto óptimo es cualquier proceso multi-paso que abarca múltiples servicios, necesita reintentos y compensación, y donde la correctitud importa más que el throughput crudo. Procesamiento de órdenes, flujos de pago, secuencias de onboarding de usuarios, orquestación de pipelines de datos, workflows de aprobación, ahí es donde Temporal brilla.\n\nPara un pipeline típico de procesamiento de órdenes, la migración toma unas seis semanas incluyendo testing. Típicamente se pasa más tiempo descomisionando la infraestructura vieja que construyendo los nuevos workflows. Eso revela dónde realmente vive la complejidad."},{"heading":"Preguntas frecuentes","level":2,"url":"https://lucioduran.com/es/blog/temporal-workflow-engine-durable-execution#questions","content":""},{"heading":"Cómo se compara Temporal con AWS Step Functions?","level":3,"url":"https://lucioduran.com/es/blog/temporal-workflow-engine-durable-execution#question-1","content":"Step Functions usa una definición de state machine basada en JSON (ASL), mientras que Temporal te deja escribir workflows como código regular en Go, TypeScript, Java o Python. Temporal te da más flexibilidad para lógica compleja (loops, condicionales, sub-workflows) y no tiene el límite de 25K eventos en el historial. Step Functions es más fácil para workflows lineales simples y tiene integración más ajustada con AWS."},{"heading":"Temporal puede reemplazar a Kafka para arquitecturas event-driven?","level":3,"url":"https://lucioduran.com/es/blog/temporal-workflow-engine-durable-execution#question-2","content":"No directamente. Temporal es para orquestación, coordinar una secuencia de pase es con reintentos y compensación. Kafka es para event streaming, broadcastear eventos a múltiples consumidores independientes. Son complementarios. Temporal puede orquestar workflows que se disparan por eventos de Kafka, combinando las fortalezas de ambos."},{"heading":"Qué pasa con los workflows corriendo cuando deployeo código nuevo?","level":3,"url":"https://lucioduran.com/es/blog/temporal-workflow-engine-durable-execution#question-3","content":"Temporal hace replay del historial de eventos del workflow contra tu código nuevo. Si tu código nuevo es deterministic-compatible con el viejo (misma secuencia de llamadas a activities, timers, etc.), el replay tiene éxito y el workflow continúa. Si cambiaste la secuencia, el replay falla con un error de no-determinismo. Utilizar workflow.patched() o versionado de task queues para manejar esto."},{"heading":"Tips","level":2,"url":"https://lucioduran.com/es/blog/temporal-workflow-engine-durable-execution#article-tips","content":"1. **Tip:** No invocar a ` Date.now() ` o ` Math.random() ` dentro de una función de workflow de Temporal. Utilizar ` workflow.now() ` y el random determinístico de ` @temporalio/workflow `. Los replays van a divergir silenciosamente, y te se va a enterar cuando un workflow falle en recuperarse después de un restart del worker, generalmente a las 3 de la mañana.\n2. **Tip:** Configurar ` startToCloseTimeout ` y ` scheduleToCloseTimeout ` en cada activity. Las activities pueden colgarse para siempre en llamadas a APIs externas sin timeout, lo que significa que el workflow también espera para siempre. Establecer 30 segundos por defecto para llamadas a API, 5 minutos para operaciones batch, y siempre agregar un ` heartbeatTimeout ` para activities de larga duración.\n3. **Tip:** Utilizar ` workflow.patched() ` para versionado en vez de intentar deployar cambios breaking en workflows. Los workflows corriendo van a hacer replay con el code path viejo mientras los nuevos usan la versión parcheada. Un intento de migración big-bang causó que 400 workflows in-flight fallaran todos simultáneamente un lunes a la mañana.\n4. **Tip:** Ejecutar el CLI ` tctl ` de Temporal con ` workflow describe -w <id> ` antes de debuggear cualquier cosa en tu código. La mitad de las veces el problema es una retry policy mal configurada o un timeout demasiado agresivo, no tu lógica de negocio.\n\n- temporal\n- durable-execution\n- workflow-engine\n- saga-pattern\n- orchestration\n- distributed-systems\n- message-queues\n- backend-architecture\n\n[Volver al Blog](<https://lucioduran.com/es/blog>)"}],"links":[{"text":"English","url":"https://lucioduran.com/blog/temporal-workflow-engine-durable-execution"},{"text":"Temporal","url":"https://temporal.io/"},{"text":"Volver al Blog","url":"https://lucioduran.com/es/blog"}],"structuredData":[{"@context":"https://schema.org","@graph":[{"@type":"BlogPosting","@id":"https://lucioduran.com/es/blog/temporal-workflow-engine-durable-execution#article","mainEntityOfPage":{"@type":"WebPage","@id":"https://lucioduran.com/es/blog/temporal-workflow-engine-durable-execution"},"headline":"Temporal.io: Reemplazando Colas de Mensajes con Ejecución Durable para Orquestación Compleja","description":"Análisis del modelo de ejecución durable de Temporal, las restricciones de determinismo de workflows, reintentos de activities, patrones saga, y por qué la ejecución durable supera a la orquestación construida sobre colas de mensajes.","datePublished":"2026-02-28","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://lucioduran.com/favicons/og-default.png","url":"https://lucioduran.com/es/blog/temporal-workflow-engine-durable-execution","inLanguage":"es","isPartOf":{"@id":"https://lucioduran.com/blog#blog"},"articleSection":"Backend","keywords":"temporal, durable-execution, workflow-engine, saga-pattern, orchestration, distributed-systems, message-queues, backend-architecture","wordCount":1797,"timeRequired":"PT10M","workTranslation":[{"@type":"BlogPosting","inLanguage":"en","url":"https://lucioduran.com/blog/temporal-workflow-engine-durable-execution"}]},{"@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":"Temporal.io: Reemplazando Colas de Mensajes con Ejecución Durable para Orquestación Compleja","item":"https://lucioduran.com/es/blog/temporal-workflow-engine-durable-execution"}]},{"@type":"FAQPage","mainEntity":[{"@type":"Question","name":"Cómo se compara Temporal con AWS Step Functions?","acceptedAnswer":{"@type":"Answer","text":"Step Functions usa una definición de state machine basada en JSON (ASL), mientras que Temporal te deja escribir workflows como código regular en Go, TypeScript, Java o Python. Temporal te da más flexibilidad para lógica compleja (loops, condicionales, sub-workflows) y no tiene el límite de 25K eventos en el historial. Step Functions es más fácil para workflows lineales simples y tiene integración más ajustada con AWS."}},{"@type":"Question","name":"Temporal puede reemplazar a Kafka para arquitecturas event-driven?","acceptedAnswer":{"@type":"Answer","text":"No directamente. Temporal es para orquestación, coordinar una secuencia de pase es con reintentos y compensación. Kafka es para event streaming, broadcastear eventos a múltiples consumidores independientes. Son complementarios. Temporal puede orquestar workflows que se disparan por eventos de Kafka, combinando las fortalezas de ambos."}},{"@type":"Question","name":"Qué pasa con los workflows corriendo cuando deployeo código nuevo?","acceptedAnswer":{"@type":"Answer","text":"Temporal hace replay del historial de eventos del workflow contra tu código nuevo. Si tu código nuevo es deterministic-compatible con el viejo (misma secuencia de llamadas a activities, timers, etc.), el replay tiene éxito y el workflow continúa. Si cambiaste la secuencia, el replay falla con un error de no-determinismo. Utilizar workflow.patched() o versionado de task queues para manejar esto."}}]}]}],"contentHash":"8ca21e98cb30b6fbb9c6d359419657d7bf3db215348af5350f1650390bcf4d7d","provenance":{"method":"Deterministic serialization of the server-rendered main element","source":"https://lucioduran.com/es/blog/temporal-workflow-engine-durable-execution","hashAlgorithm":"sha256","hashScope":"JSON document fields before contentHash and provenance","sourceHTMLBytes":73333,"contentBytes":17262}}
