TL;DR — Resumen Rápido

Domina Temporal para orquestacion de workflows durables en microservicios. Arquitectura, instalacion, SDKs, patron saga y ejemplo de procesamiento de pedidos.

TEMPORAL — ORQUESTACION DE WORKFLOWS DURABLES Cliente SDK Start / Signal Query Temporal Server Frontend gRPC / HTTP History Event sourcing Matching Task queues Persistencia MySQL / Postgres Aislamiento por Namespace Proceso Worker Ejecutor Workflow Ejecutor Activity Proceso Worker Escala independiente Servicios Externos APIs / BDs / Colas Temporal UI localhost:8080 Historial de eventos Los Workers consultan task queues — Temporal Server nunca empuja trabajo

Los sistemas distribuidos fallan de forma parcial — un servicio de pago agota el tiempo, un registro de envio se escribe pero la confirmacion nunca llega, y ahora los datos son inconsistentes en tres bases de datos sin forma de revertirlo. Temporal resuelve esta clase de problema haciendo que los workflows sean durables por defecto, usando event sourcing para sobrevivir crashes, replays para recuperar estado y politicas de reintento estructuradas para manejar fallos transitorios. Esta guia cubre el stack completo de Temporal: arquitectura del servidor, opciones de instalacion, primitivos de workflow y activity en Go, TypeScript y Python, patrones avanzados como saga y flujos con intervencion humana, y un ejemplo completo de procesamiento de pedidos.

Requisitos Previos

  • Docker y Docker Compose instalados (para Temporal Server local)
  • Go 1.22+, Node.js 22+, o Python 3.11+ segun el SDK elegido
  • Familiaridad basica con microservicios y sistemas distribuidos
  • Comprension de patrones de programacion asincrona (promesas, goroutines o asyncio)

Arquitectura de Temporal

El Temporal Server esta compuesto por cuatro servicios internos que pueden ejecutarse como un unico binario o de forma independiente para escalar:

Frontend — la puerta de enlace gRPC y HTTP expuesta al exterior. Los Clientes y Workers se conectan aqui para iniciar workflows, enviar signals, ejecutar queries y consultar task queues.

History — el nucleo de Temporal. Persiste cada evento del workflow en la base de datos y dirige la ejecucion reproduciendo el historial de eventos. Cada ejecucion de workflow es gestionada por un unico fragmento de History, garantizando un ordenamiento riguroso.

Matching — gestiona los task queues. Cuando el servicio History necesita que se ejecute una tarea de workflow o activity, la empuja a Matching, que la retiene hasta que un Worker la consulte. Este modelo pull significa que los Workers nunca se sobrecargan.

Worker Interno — ejecuta los workflows del propio sistema Temporal para gestion de namespaces, archivado y replicacion.

Los Workers son tus procesos de aplicacion — contienen el codigo de tus workflows y activities. Los Workers consultan task queues nombrados desde el Temporal Server, ejecutan el trabajo localmente y devuelven resultados. Los Workers son sin estado y escalables horizontalmente.

Event Sourcing y Replay: cada workflow mantiene un historial de eventos completo y ordenado en la base de datos. Si un Worker falla a mitad de un workflow, un nuevo Worker recoge la tarea, reproduce el historial para reconstruir el estado exacto en memoria y continua la ejecucion desde el ultimo punto de control duradero.

Instalacion

Docker Compose (desarrollo local)

git clone https://github.com/temporalio/docker-compose.git
cd docker-compose
docker compose up

Esto inicia:

  • temporal — Temporal Server en el puerto 7233 (gRPC)
  • temporal-ui — Interfaz web en el puerto 8080
  • temporal-admin-tools — contenedor con tctl CLI preinstalado
  • postgresql — backend de persistencia
# Acceder a tctl via el contenedor admin-tools
docker exec -it temporal-admin-tools tctl namespace register --retention 7 default

# Listar workflows en ejecucion
docker exec -it temporal-admin-tools tctl workflow list

Kubernetes con Helm

helm repo add temporal https://go.temporal.io/helm-charts
helm repo update

helm install temporal temporal/temporal \
  --set server.replicaCount=3 \
  --set cassandra.config.cluster_size=3 \
  --set elasticsearch.enabled=true \
  --namespace temporal \
  --create-namespace

Temporal Cloud (gestionado)

Temporal Cloud elimina la carga operativa. Obtienes un endpoint de namespace, certificados mTLS y facturacion por uso. Conecta via el SDK con tu endpoint y certificados:

TEMPORAL_ADDRESS=<namespace>.tmprl.cloud:7233
TEMPORAL_TLS_CERT=ruta/al/client.pem
TEMPORAL_TLS_KEY=ruta/al/client.key

Conceptos Fundamentales

Workflow — Una funcion determinista que orquesta activities, timers, signals y workflows hijo. Debe ser determinista: sin numeros aleatorios, sin llamadas al sistema directo, sin acceder a estado global mutable.

Activity — Una funcion que realiza efectos secundarios no deterministas: llamadas HTTP, escrituras en base de datos, E/S de archivos, envio de correos. Las Activities se ejecutan en Workers y tienen politicas de reintento configurables.

Signal — Un evento externo enviado a un workflow en ejecucion. Los Signals permiten que sistemas externos inserten datos en un workflow en curso (por ejemplo, “pago aprobado”, “usuario cancelo pedido”).

Query — Una lectura sincrona del estado actual de un workflow sin afectar su ejecucion.

Task Queue — Un canal nombrado a traves del cual el Temporal Server distribuye trabajo a los Workers.

Namespace — Un limite de aislamiento para workflows, con configuracion de retencion, politicas de seguridad y esquemas de atributos de busqueda independientes.

Escritura de Workflows en Go

package order

import (
    "time"
    "go.temporal.io/sdk/workflow"
)

var defaultRetryPolicy = &temporal.RetryPolicy{
    InitialInterval:    time.Second,
    BackoffCoefficient: 2.0,
    MaximumInterval:    30 * time.Second,
    MaximumAttempts:    5,
    NonRetryableErrorTypes: []string{"InvalidOrderError"},
}

func OrderWorkflow(ctx workflow.Context, order OrderInput) (OrderResult, error) {
    ao := workflow.ActivityOptions{
        StartToCloseTimeout: 30 * time.Second,
        RetryPolicy:         defaultRetryPolicy,
    }
    ctx = workflow.WithActivityOptions(ctx, ao)

    var paymentResult PaymentResult
    err := workflow.ExecuteActivity(ctx, ValidatePayment, order).Get(ctx, &paymentResult)
    if err != nil {
        return OrderResult{}, err
    }

    var inventoryResult InventoryResult
    err = workflow.ExecuteActivity(ctx, ReserveInventory, order).Get(ctx, &inventoryResult)
    if err != nil {
        workflow.ExecuteActivity(ctx, RefundPayment, paymentResult).Get(ctx, nil)
        return OrderResult{}, err
    }

    // Esperar signal de envio con timeout de 24 horas
    signalChan := workflow.GetSignalChannel(ctx, "shipping-update")
    var shippingInfo ShippingInfo
    selector := workflow.NewSelector(ctx)
    selector.AddReceive(signalChan, func(c workflow.ReceiveChannel, _ bool) {
        c.Receive(ctx, &shippingInfo)
    })
    timerFuture := workflow.NewTimer(ctx, 24*time.Hour)
    selector.AddFuture(timerFuture, func(f workflow.Future) {
        shippingInfo.Status = "timeout"
    })
    selector.Select(ctx)

    // Sleep durable — sobrevive reinicios del Worker
    workflow.Sleep(ctx, 7*24*time.Hour)

    workflow.ExecuteActivity(ctx, SendDeliveryConfirmation, order, shippingInfo).Get(ctx, nil)
    return OrderResult{OrderID: order.ID, Status: "completed"}, nil
}

Versionado de Workflows con GetVersion

func OrderWorkflow(ctx workflow.Context, order OrderInput) (OrderResult, error) {
    v := workflow.GetVersion(ctx, "add-fraud-check", workflow.DefaultVersion, 1)
    if v >= 1 {
        workflow.ExecuteActivity(ctx, FraudCheck, order).Get(ctx, nil)
    }
    // ... resto del workflow
}

Escritura de Workflows en TypeScript

import { proxyActivities, sleep, setHandler, defineSignal,
         defineQuery, condition } from '@temporalio/workflow';

const { validatePayment, reserveInventory, refundPayment,
        sendDeliveryConfirmation } = proxyActivities<Activities>({
  startToCloseTimeout: '30 seconds',
  retry: {
    initialInterval: '1s',
    backoffCoefficient: 2,
    maximumInterval: '30s',
    maximumAttempts: 5,
    nonRetryableErrorTypes: ['InvalidOrderError'],
  },
});

const shippingSignal = defineSignal<[ShippingInfo]>('shipping-update');
const orderStatusQuery = defineQuery<string>('order-status');

export async function orderWorkflow(order: OrderInput): Promise<OrderResult> {
  let currentStatus = 'processing';

  setHandler(shippingSignal, (info: ShippingInfo) => {
    currentStatus = `shipped:${info.trackingId}`;
  });
  setHandler(orderStatusQuery, () => currentStatus);

  const payment = await validatePayment(order);

  let inventory;
  try {
    inventory = await reserveInventory(order);
  } catch (err) {
    await refundPayment(payment);
    throw err;
  }

  const signalReceived = await condition(
    () => currentStatus.startsWith('shipped:'),
    '24 hours'
  );
  if (!signalReceived) currentStatus = 'shipping-timeout';

  await sleep('7 days');
  await sendDeliveryConfirmation(order, currentStatus);
  return { orderId: order.id, status: 'completed' };
}

Escritura de Workflows en Python

from datetime import timedelta
from temporalio import workflow, activity
from temporalio.common import RetryPolicy

@workflow.defn
class OrderWorkflow:
    def __init__(self) -> None:
        self._status = "processing"
        self._shipping_info = None

    @workflow.run
    async def run(self, order: OrderInput) -> OrderResult:
        retry_policy = RetryPolicy(
            initial_interval=timedelta(seconds=1),
            backoff_coefficient=2.0,
            maximum_interval=timedelta(seconds=30),
            maximum_attempts=5,
            non_retryable_error_types=["InvalidOrderError"],
        )
        payment = await workflow.execute_activity(
            validate_payment, order,
            start_to_close_timeout=timedelta(seconds=30),
            retry_policy=retry_policy,
        )
        try:
            inventory = await workflow.execute_activity(
                reserve_inventory, order,
                start_to_close_timeout=timedelta(seconds=30),
                retry_policy=retry_policy,
            )
        except Exception:
            await workflow.execute_activity(refund_payment, payment,
                start_to_close_timeout=timedelta(seconds=30))
            raise

        await workflow.wait_condition(
            lambda: self._shipping_info is not None,
            timeout=timedelta(hours=24),
        )
        await workflow.sleep(timedelta(days=7))
        await workflow.execute_activity(send_delivery_confirmation, order,
            start_to_close_timeout=timedelta(seconds=30))
        return OrderResult(order_id=order.id, status="completed")

    @workflow.signal
    def shipping_update(self, info: ShippingInfo) -> None:
        self._shipping_info = info

    @workflow.query
    def order_status(self) -> str:
        return self._status

Patrones de Activity

Heartbeating para Activities Largas

Las Activities deben enviar heartbeats para indicar a Temporal que siguen activas. Si un Worker falla, el timeout de heartbeat activa la reprogramacion en otro Worker:

func ProcessLargeFile(ctx context.Context, fileURL string) error {
    for i, chunk := range chunks {
        activity.RecordHeartbeat(ctx, fmt.Sprintf("chunk %d/%d", i+1, len(chunks)))
        if ctx.Err() != nil {
            return ctx.Err()
        }
        processChunk(chunk)
    }
    return nil
}

Activities Locales

Las Activities Locales se ejecutan en el mismo proceso Worker que el Workflow, sin ida y vuelta al Temporal Server. Usaras para operaciones rapidas (menos de un segundo) que aun necesitan reintentos:

lao := workflow.LocalActivityOptions{StartToCloseTimeout: 5 * time.Second}
ctx = workflow.WithLocalActivityOptions(ctx, lao)
workflow.ExecuteLocalActivity(ctx, FormatOrderID, order).Get(ctx, &formattedID)

Patrones de Workflow

Patron Saga para Transacciones Distribuidas

func OrderSagaWorkflow(ctx workflow.Context, order OrderInput) error {
    var compensations []func(workflow.Context) error

    ao := workflow.ActivityOptions{StartToCloseTimeout: 30 * time.Second}
    ctx = workflow.WithActivityOptions(ctx, ao)

    var payment PaymentResult
    if err := workflow.ExecuteActivity(ctx, ChargePayment, order).Get(ctx, &payment); err != nil {
        return err
    }
    compensations = append(compensations, func(ctx workflow.Context) error {
        return workflow.ExecuteActivity(ctx, RefundPayment, payment).Get(ctx, nil)
    })

    var reservation InventoryReservation
    if err := workflow.ExecuteActivity(ctx, ReserveInventory, order).Get(ctx, &reservation); err != nil {
        for i := len(compensations) - 1; i >= 0; i-- {
            compensations[i](ctx)
        }
        return err
    }
    compensations = append(compensations, func(ctx workflow.Context) error {
        return workflow.ExecuteActivity(ctx, ReleaseInventory, reservation).Get(ctx, nil)
    })

    if err := workflow.ExecuteActivity(ctx, CreateShipment, order, reservation).Get(ctx, nil); err != nil {
        for i := len(compensations) - 1; i >= 0; i-- {
            compensations[i](ctx)
        }
        return err
    }
    return nil
}

Workflows Programados con CronSchedule

c.ExecuteWorkflow(ctx,
    client.StartWorkflowOptions{
        ID:           "reporte-diario",
        TaskQueue:    "reportes",
        CronSchedule: "0 9 * * MON-FRI",
    },
    DailyReportWorkflow,
    ReportInput{ReportType: "ventas"},
)

Namespaces y Visibilidad

# Crear namespace con retencion de 30 dias
tctl namespace register \
  --retention 30 \
  --description "Procesamiento de pedidos en produccion" \
  pedidos-produccion

# Agregar atributos de busqueda personalizados
tctl admin cluster add-search-attributes \
  --name EstadoPedido --type Text \
  --name NivelCliente --type Keyword \
  --name MontoPedido --type Double

# Listar workflows con filtro avanzado
tctl workflow list \
  --query 'EstadoPedido="pendiente" AND NivelCliente="premium" ORDER BY StartTime DESC'

Temporal UI

La interfaz de Temporal en localhost:8080 ofrece:

  • Lista de Workflows — tabla con buscador de todas las ejecuciones con estado, hora de inicio y task queue
  • Detalle de Ejecucion — historial completo de eventos con cada transicion de estado, marcas de tiempo y payloads
  • Stack Trace — muestra en que punto del codigo esta bloqueado actualmente el workflow
  • Activities Pendientes — lista activities programadas pero no iniciadas aun

Comparativa de Herramientas

CaracteristicaTemporalApache AirflowAWS Step FunctionsPrefectInngestConductor
Uso principalWorkflows durables en microserviciosDAGs de pipelines de datosMaquinas de estado serverlessOrquestacion de datosFunciones event-drivenOrquestacion de microservicios
Modelo de ejecucionDuradero de larga duracionEjecuciones batch DAGServerless gestionadoEjecuciones de flujoPasos serverlessMotor de workflow
LenguajeGo, Java, TS, Python, .NETDAGs PythonJSON/YAML DSLPythonTypeScriptJSON/Java
Replay/DurabilidadEvent sourcing completoNingunoGestionado por AWSBasado en checkpointsLimitadoLimitado
Signals/QueriesSi — nativosNoSolo callbacksNoSolo eventosSignals
Dev localDocker ComposeDocker ComposeRequiere AWSServidor localServidor devDocker
Nube gestionadaTemporal CloudMWAANativoPrefect CloudSiConductor Cloud
Mejor paraWorkflows complejos y de larga duracionPipelines ETLWorkflows AWS simplesPipelines ML/datosCadenas de eventos serverlessCoreografia de microservicios

Ejemplo Practico: Saga de Procesamiento de Pedidos

// workflows/order-saga.ts
import { proxyActivities, sleep, setHandler, defineSignal,
         defineQuery, condition } from '@temporalio/workflow';

const { chargePayment, refundPayment, reserveInventory, releaseInventory,
        createShipment, sendConfirmationEmail } = proxyActivities<Activities>({
  startToCloseTimeout: '60 seconds',
  retry: { maximumAttempts: 3, initialInterval: '2s', backoffCoefficient: 2 },
});

const cancelSignal = defineSignal('cancel-order');
const statusQuery = defineQuery<string>('status');

export async function orderSagaWorkflow(order: OrderInput): Promise<OrderResult> {
  let status = 'received';
  let cancelled = false;

  setHandler(cancelSignal, () => { cancelled = true; });
  setHandler(statusQuery, () => status);

  status = 'charging';
  const payment = await chargePayment(order);

  if (cancelled) {
    await refundPayment(payment);
    return { orderId: order.id, status: 'cancelled' };
  }

  status = 'reserving';
  let inventory;
  try {
    inventory = await reserveInventory(order);
  } catch (err) {
    await refundPayment(payment);
    throw err;
  }

  status = 'shipping';
  try {
    await createShipment(order, inventory);
  } catch (err) {
    await releaseInventory(inventory);
    await refundPayment(payment);
    throw err;
  }

  status = 'awaiting-delivery';
  const delivered = await condition(() => status === 'delivered', '30 days');
  if (!delivered) status = 'delivery-timeout';

  await sendConfirmationEmail(order, status);
  return { orderId: order.id, status };
}
# Iniciar el workflow
tctl workflow start \
  --taskqueue procesamiento-pedidos \
  --workflow_type orderSagaWorkflow \
  --workflow_id "pedido-12345" \
  --input '{"id":"12345","items":[{"sku":"PROD-001","qty":2}]}'

# Consultar estado actual
tctl workflow query --workflow_id pedido-12345 --query_type status

# Enviar signal de confirmacion de entrega
tctl workflow signal \
  --workflow_id pedido-12345 \
  --name delivered \
  --input '{"deliveredAt":"2026-03-23T14:00:00Z"}'

Errores Comunes y Precauciones

Los bugs de no-determinismo son el problema mas frecuente en Temporal. Cualquier codigo que produzca resultados diferentes en el replay corrompe el estado del workflow. Nunca uses time.Now(), rand, UUIDs ni llamadas directas a APIs dentro de funciones de workflow — usa siempre workflow.Now() y workflow.GetVersion().

Los heartbeats ausentes en activities largas provocan que la activity se reprograme aunque siga ejecutandose, creando ejecuciones duplicadas. Siempre envia heartbeats en bucles y verifica ctx.Err() tras cada uno.

El historial de eventos ilimitado se acumula cuando un workflow se ejecuta indefinidamente sin punto de control. Usa Continue-As-New para bucles de sondeo y procesos de larga duracion.

La discrepancia en el task queue — los Workers y los inicios de workflow deben usar el mismo nombre de task queue. Una errata significa que la tarea del workflow espera en la cola sin Worker que la recoja.

Resumen

  • El modelo de event sourcing de Temporal hace los workflows durables por defecto — los crashes, despliegues e interrupciones de red no pierden el estado del workflow
  • Los Workers consultan task queues — el modelo pull significa que los Workers nunca se sobrecargan y escalan independientemente del servidor
  • Las Activities manejan todos los efectos secundarios no deterministas con politicas de reintento configurables, heartbeats y controles de timeout
  • Signals y Queries permiten que sistemas externos interactuen con workflows en ejecucion sin sondear tu base de datos
  • El patron Saga con Activities compensatorias es el enfoque nativo de Temporal para transacciones distribuidas
  • GetVersion habilita despliegues progresivos seguros sin romper ejecuciones de workflow en curso
  • Usa Temporal Cloud en produccion para eliminar la carga operativa del servidor

Articulos Relacionados