Procesos orquestados fiables y observables en servicios distribuidos con Temporal

Escrito por picodotdev el .
java programacion
Enlace permanente Comentarios

Temporal es una herramienta open source que permite modelar procesos de larga duración directamente en código, con garantías de fiabilidad y resiliencia ante fallos. En este artículo explico sus conceptos fundamentales, workflows, actividades y señales, y los pongo en práctica implementando en Java un proceso de compra de entradas con múltiples microservicios implicados.

Temporal

Desde hace tiempo tenía pendiente investigar y entender Cadence, tras un tiempo leyendo su documentación no tenía claro cómo funcionaba, qué funcionalidad cubría y como funcionaba. Sin embargo, al preguntarle a Claude me indicó, como ocurre ocurre en muchos casos, los desarrolladores originales han creado un fork, en el caso de Cadence el fork es Temporal.

Tras unos días y siguiendo la buena documentación ofrecida por temporal tengo un ejemplo con el que entender, practicar y usar un pequeño proceso. Con la documentación y el ejemplo pensar en posibles casos de uso al dar a solución a necesidades.

En la web dan algunos ejemplos de casos de uso de Temporal y en su página de inicio una descripción introductoria de la herramienta.

Procesos con Temporal

De forma muy resumida Temporal es una herramienta que permite modelar procesos que pueden durar minutos, días, meses o años e integrar interacciones humanas. Además, la implementación de los procesos se realiza con garantías de fiabilidad y resiliencia al considerar que cualquier paso del proceso puede fallar e implementando políticas de reintentos.

Temporal tiene otras características diferenciadoras. Las herramientas tradicionales para modelar procesos se basan en la definición de un proceso en una notación textual como xml y su entorno de ejecución es el servidor de procesos. En Temporal por el contrario el proceso se define en código en alguno de los diferentes lenguajes de programación soportados con mayores capacidades que un archivo descriptor.

Sus casos de uso son los siguientes:

  • Máquinas de estados complejas: Modelar entidades de negocio con ciclos de vida largos y múltiples transiciones de estado, como un pedido que pasa por creado, reservado, pagado, enviado y entregado a lo largo de días.
  • Procesos de larga duración con esperas humanas: Flujos de aprobación, onboarding de usuarios o procesos de KYC donde el workflow queda en espera de una acción humana durante horas, días, semanas, meses o años y luego continúa.
  • Sagas y transacciones distribuidas: Implementar el patrón saga para gestionar la consistencia eventual entre microservicios con compensaciones automáticas si algún paso falla, evitando inconsistencias de datos.
  • Tareas programadas y recurrentes: Con la funcionalidad de schedule, sustituir cronjobs frágiles por procesos planificados con reintentos, historial y observabilidad.

Casos de no uso son los siguientes:

  • Procesos simples y de corta duración: si el proceso se resuelve en una sola llamada HTTP o una transacción de base de datos, Temporal añade una complejidad innecesaria. Para esos casos un simple servicio REST o una cola de mensajes como Kafka es suficiente.
  • Alta frecuencia y baja latencia: Temporal no está diseñado para procesos que requieren respuesta en milisegundos o millones de ejecuciones por segundo, como procesamiento de eventos en tiempo real o sistemas de trading de alta frecuencia.
  • Equipos pequeños sin necesidad de resiliencia distribuida: Si la aplicación es un monolito o un sistema sencillo, introducir Temporal implica operar un servidor adicional con su base de datos, lo que puede no justificarse.
  • Lógica puramente reactiva a eventos: Si el sistema ya está bien modelado con event sourcing y CQRS y los procesos son stateless, herramientas como Kafka Streams o Apache Flink encajan mejor de forma natural.
  • Procesos con requisitos de latencia estrictos en cada actividad: Temporal introduce overhead en la coordinación entre el servidor y los workers, por lo que si cada actividad del proceso tiene un SLA de milisegundos, ese overhead puede ser un problema.

Conceptos

Temporal define los siguientes conceptos.

  • Workflows que representan los procesos y se implementan en código de un lenguaje de programación. En Java los workflows tienen la definición de una interfaz con un método que inicia el proceso, métodos para recibir mensajes que permiten consultar el estado del proceso y cambiar el estado. El proceso en su implementación invoca los pasos del proceso o actividades.
  • Las actividades son los pasos del proceso, se ejecutan como consecuencia del progreso del proceso, cambio estado o de los mensajes que recibe el proceso.
  • Las señales son la forma de comunicarse con el proceso, obtener información, cambiar el flujo de ejecución de forma síncrona y asíncrona.
  • Finalmente los workers, son las unidades de cómputo que ejecutan el proceso y las actividades.

Temporal es un binario que hace de servidor coordinando la ejecución de los procesos, gestionando los cambios de estado y almacenando el estado del proceso. Registra el inicio de una nueva instancia de un proceso, los datos de entrada y salida de cada actividad junto a otros metadatos como cuanto tiempo tarda cada paso.

En modo desarrollo el estado de los procesos se almacena en memoria. En un entorno de producción el estado se aconseja guardar en una base de datos como PostgreSQL.

Esta supervisión de los procesos permite a Temporal dar visibilidad de los procesos que es una característica que modelan los procesos con coreografía está distribuída entre los diferentes procesos.

Funcionalidades

Temporal soporta diferentes funcionalidades, en la documentación se detallan entre las que están:

  • Schedule: permite planificar procesos cada cierto tiempo.
  • Composability: es posible crear e invocar subprocesos.
  • Encryption: permite cifrar los datos del proceso, algunos de los datos pueden ser sensibles.
  • Observability: se recogen métricas a raíz del estado del proceso.
  • Testing: es posible crear diferentes tipos de teses unitarios, de integración y end-to-end.

Definición de un proceso

Supongamos que tenemos un proceso de compra de entradas. El sistema ha sido desarrollado utilizando domain driven design y microservicios separando las funciones del sistema en diferentes subdominios y se compone del flujo hasta que el order queda en estado confirmada.

Diagrama de secuencia

Diagrama de secuencia

Tras el estado de confirmación o cancelación de la orden comenzaría un siguiente proceso, subprocesos asociados o faltaría definir más pasos en el proceso hasta que el producto o servicio por el que se ha pagado ha sido entregado al cliente. Para el ejemplo es suficiente hasta confirmación.

Implementar este proceso en un sistema distribuido de microservicios en el que hay múltiples puntos de fallo basta que alguna de las actividades temporalmente no funcione para generar inconsistencias de datos aún con reintentos en los procesos de esta coreografía de microservicios.

Que pasa si, ¿se reservan los tickets, falla la autorización y no se pueden liberar los tickets temporalmente? o ¿si se crea el order pero no se puede notificar la decisión al sistema de fraude si finalmente se acepta la transacción?. Además al estar el proceso distribuido en diferentes servicios es difícil tener visibilidad con información completa del estado del proceso y pasos por los que ha pasado.

Generalmente un sistema con coreografía funciona pero en ciertos errores el proceso deja un estado inconsistente las transacciones, investigar y resolver las inconsistencias de datos, si la resolución es manual es una actividad que consume gran cantidad de tiempo.

Temporal proporciona soluciones a estos problemas a un proceso vital en cualquier dominio como son las transacciones como este en el proceso de compra.

Ejemplo con Temporal

Esta es la definición del proceso en el lenguaje de programación Java. El proceso es simplemente una interfaz con anotaciones.

Workflow

La definición del workflow.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package io.github.picodotdev.blogbitix.temporal.workflow;

import io.temporal.workflow.WorkflowInterface;
import io.temporal.workflow.WorkflowMethod;

@WorkflowInterface
public interface PlaceOrderWorkflow {

    @WorkflowMethod
    void placeOrder(Long listingId, int quantity);
}
PlaceOrderWorkflow.java

Esta es la implementación del proceso. Contiene las diferentes actividades del proceso que invocan diferentes sistemas. No contiene más unas pocas sentencias de código haciendo uso de las actividades del proceso. Como si fuera una secuencia de intrucciones u orquestado en un patrón saga.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package io.github.picodotdev.blogbitix.temporal.workflow;

import io.temporal.workflow.WorkflowInterface;
import io.temporal.workflow.WorkflowMethod;

@WorkflowInterface
public interface PlaceOrderWorkflow {

    @WorkflowMethod
    void placeOrder(Long listingId, int quantity);
}
PlaceOrderWorkflow.java

Activities

La definición en interfaces.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package io.github.picodotdev.blogbitix.temporal.activities;

import io.temporal.activity.ActivityInterface;
import io.temporal.activity.ActivityMethod;

@ActivityInterface
public interface FraudActivities {

    @ActivityMethod
    FraudDecide decide(Long orderId);

    @ActivityMethod
    void authorizeFailure(Long orderId);

    @ActivityMethod
    void decision(Long orderId, Long auhtorizeId, FraudDecision decision);

    enum FraudDecide {
        APPROVE, REJECT
    }

    enum FraudDecision {
        APPROVE, REJECT
    }
}
FraudActivities.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package io.github.picodotdev.blogbitix.temporal.activities;

import io.temporal.activity.ActivityInterface;
import io.temporal.activity.ActivityMethod;

@ActivityInterface
public interface InventoryActivities {

    @ActivityMethod
    void bookTickets(Long listingId, int quantity) throws ReserveTicketsException;

    @ActivityMethod
    void releaseTickets(Long listingId, int quantity) throws ReleaseTicketsException;
}
InventoryActivities.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
package io.github.picodotdev.blogbitix.temporal.activities;

import io.temporal.activity.ActivityInterface;
import io.temporal.activity.ActivityMethod;

@ActivityInterface
public interface OrderActivities {

    @ActivityMethod
    Long generateOrderId();

    @ActivityMethod
    void createOrder(Long orderId, Long listingId, int quantity);

    @ActivityMethod
    OrderStatus approveOrder(Long orderId);

    enum OrderStatus {
        CREATED, APPROVED, CANCELLED
    }
}
OrderActivities.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package io.github.picodotdev.blogbitix.temporal.activities;

import io.temporal.activity.ActivityInterface;
import io.temporal.activity.ActivityMethod;

@ActivityInterface
public interface PaymentActivities {

    @ActivityMethod
    Long authorizePayment();
}
PaymentActivities.java

La implementación. Para el caso del ejemplo las actividades no invocan sistemas externos, simplemente emiten un mensaje. Los procesos han de ser reproducibles y reconstruibles, por tanto algunas funcionalidades han de usar las facilidades de Temporal, especialmente en el manejo de tiempo y datos aleatorios.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package io.github.picodotdev.blogbitix.temporal.activities;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class DefaultFraudActivities implements FraudActivities {

    private static final Logger logger = LogManager.getLogger(DefaultFraudActivities.class);

    @Override
    public FraudDecide decide(Long orderId) {
        logger.info("Fraud decide (orderId: {})", orderId);
        return FraudDecide.APPROVE;
    }

    @Override
    public void authorizeFailure(Long orderId) {
        logger.info("Fraud authorize failure (orderId: {})", orderId);
    }

    @Override
    public void decision(Long orderId, Long authorizeId, FraudDecision decision) {
        logger.info("Fraud decision (orderId: {}, authorizeId: {}, decision: {})", orderId, authorizeId, decision);
    }
}
DefaultFraudActivities.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
package io.github.picodotdev.blogbitix.temporal.activities;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class DefaultInventoryActivities implements InventoryActivities {

    private static final Logger logger = LogManager.getLogger(DefaultInventoryActivities.class);

    @Override
    public void bookTickets(Long listingId, int quantity) throws ReserveTicketsException {
        logger.info("Inventory book tickets (listingId: {}, quantity: {})", listingId, quantity);
    }

    @Override
    public void releaseTickets(Long listingId, int quantity) throws ReleaseTicketsException {
        logger.info("Inventory release tickets (listingId: {}, quantity: {})", listingId, quantity);
    }
}
DefaultInventoryActivities.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package io.github.picodotdev.blogbitix.temporal.activities;

import java.util.Random;

import io.temporal.workflow.Workflow;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class DefaultOrderActivities implements OrderActivities {

    private static final Logger logger = LogManager.getLogger(DefaultOrderActivities.class);

    @Override
    public Long generateOrderId() {
        Long orderId = new Random().nextLong(0, 1_000_000);
        logger.info("Order generate id (orderId: {})", orderId);
        return orderId;
    }

    @Override
    public void createOrder(Long orderId, Long listingId, int quantity) {
        logger.info("Order create (orderId: {}, listingId: {}, quantity; {})", orderId, listingId, quantity);
    }

    @Override
    public OrderStatus approveOrder(Long orderId) {
        logger.info("Order approve (orderId: {})", orderId);
        return OrderStatus.APPROVED;
    }
}
DefaultOrderActivities.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
package io.github.picodotdev.blogbitix.temporal.activities;

import java.util.Random;

import io.temporal.workflow.Workflow;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class DefaultPaymentActivities implements PaymentActivities {

    private static final Logger logger = LogManager.getLogger(DefaultPaymentActivities.class);

    @Override
    public Long authorizePayment() {
        Long paymentId = new Random().nextLong(0, 1_000_000);
        logger.info("Payments authorize (paymentId: {})", paymentId);
        return paymentId;
    }
}
DefaultPaymentActivities.java

Worker

El worker registra que workflows y las activities va a ejecutar.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package io.github.picodotdev.blogbitix.temporal.workers;

import io.temporal.client.WorkflowClient;
import io.temporal.serviceclient.WorkflowServiceStubs;
import io.temporal.worker.Worker;
import io.temporal.worker.WorkerFactory;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

...

import static io.github.picodotdev.blogbitix.temporal.Main.PLACE_ORDER_WORKFLOW_TASK_QUEUE;

public class PlaceOrderWorkflowWorker {

    private static final Logger logger = LogManager.getLogger(PlaceOrderWorkflowWorker.class);

    private WorkflowServiceStubs service;
    private WorkflowClient client;
    private WorkerFactory factory;

    public PlaceOrderWorkflowWorker() {
        this.service = WorkflowServiceStubs.newLocalServiceStubs();
        this.client = WorkflowClient.newInstance(service);
        this.factory = WorkerFactory.newInstance(client);
    }

    public void start() {
        Worker worker = factory.newWorker(PLACE_ORDER_WORKFLOW_TASK_QUEUE);
        worker.registerWorkflowImplementationTypes(DefaultPlaceOrderWorkflow.class);
        worker.registerActivitiesImplementations(new DefaultFraudActivities());
        worker.registerActivitiesImplementations(new DefaultOrderActivities());
        worker.registerActivitiesImplementations(new DefaultPaymentActivities());
        worker.registerActivitiesImplementations(new DefaultInventoryActivities());

        logger.info("Starting PlaceOrderWorkflowWorker...");

        factory.start();
    }

    public void shutdown() {
        factory.shutdown();
    }
}
PlaceOrderWorkflowWorker.java

Programa

Luego arranca una instancia el worker y el workflow,

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package io.github.picodotdev.blogbitix.temporal;

...

@SpringBootApplication
public class Main implements ApplicationRunner {

    public static final String PLACE_ORDER_WORKFLOW_TASK_QUEUE = "place-order-workflow-task-queue";

    public static void main(String[] args) {
        SpringApplication.run(Main.class, args);
    }

    @Override
    public void run(ApplicationArguments args) throws Exception {
        PlaceOrderWorkflowWorker worker = new PlaceOrderWorkflowWorker();
        Workflow workflow = new Workflow();

        ExecutorService executor = Executors.newFixedThreadPool(3);
        executor.submit(() -> worker.start());
        Thread.sleep(1000);
        executor.submit(() -> workflow.run());
        Thread.sleep(5000);

        worker.shutdown();
        workflow.shutdown();
        executor.shutdown();
    }
}
Main.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package io.github.picodotdev.blogbitix.temporal.workflow;

import java.util.UUID;

import io.temporal.client.WorkflowClient;
import io.temporal.client.WorkflowOptions;
import io.temporal.serviceclient.WorkflowServiceStubs;

import static io.github.picodotdev.blogbitix.temporal.Main.PLACE_ORDER_WORKFLOW_TASK_QUEUE;

public class Workflow {

    private WorkflowServiceStubs service;
    private WorkflowClient client;

    public Workflow() {
        this.service = WorkflowServiceStubs.newLocalServiceStubs();
        this.client = WorkflowClient.newInstance(service);
    }

    public void run() {
        UUID uuid = UUID.randomUUID();
        PlaceOrderWorkflow workflow = client.newWorkflowStub(PlaceOrderWorkflow.class,
                                                             WorkflowOptions.newBuilder()
                                                                            .setTaskQueue(PLACE_ORDER_WORKFLOW_TASK_QUEUE)
                                                                            .setWorkflowId(uuid.toString())
                                                                            .build()
                                                            );

        workflow.placeOrder(1L, 3);
    }

    public void shutdown() {
        service.shutdown();
        service.awaitTermination(10, java.util.concurrent.TimeUnit.SECONDS);
    }
}
Workflow.java

Servicio de Temporal

El servicio de temporal se inicia con el siguiente comando en modo desarrollo.

1
2
temporal server start-dev

temporal.sh

Esta es la salida en la consola de la ejecución del workflow.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
2026-05-07 20:19:25,156  INFO             io.temporal.serviceclient.WorkflowServiceStubsImpl Created WorkflowServiceStubs for channel: ManagedChannelOrphanWrapper{delegate=ManagedChannelImpl{logId=1, target=127.0.0.1:7233}}
2026-05-07 20:19:25,244  INFO             io.temporal.serviceclient.WorkflowServiceStubsImpl Created WorkflowServiceStubs for channel: ManagedChannelOrphanWrapper{delegate=ManagedChannelImpl{logId=3, target=127.0.0.1:7233}}
2026-05-07 20:19:25,278  INFO   codotdev.blogbitix.temporal.workers.PlaceOrderWorkflowWorker Starting PlaceOrderWorkflowWorker...
2026-05-07 20:19:25,382  INFO                io.temporal.internal.worker.MultiThreadedPoller start: MultiThreadedPoller{name=Workflow Poller taskQueue="place-order-workflow-task-queue", namespace="default", identity=691@localhost}
2026-05-07 20:19:25,386  INFO                io.temporal.internal.worker.MultiThreadedPoller start: MultiThreadedPoller{name=Activity Poller taskQueue="place-order-workflow-task-queue", namespace="default", identity=691@localhost}
2026-05-07 20:19:26,453  INFO   odotdev.blogbitix.temporal.activities.DefaultOrderActivities Order generate id (orderId: 196969)
2026-05-07 20:19:26,470  INFO   odotdev.blogbitix.temporal.activities.DefaultFraudActivities Fraud decide (orderId: 196969)
2026-05-07 20:19:26,491  INFO   dev.blogbitix.temporal.activities.DefaultInventoryActivities Inventory book tickets (listingId: 1, quantity: 3)
2026-05-07 20:19:26,497  INFO   otdev.blogbitix.temporal.activities.DefaultPaymentActivities Payments authorize (paymentId: 848778)
2026-05-07 20:19:26,504  INFO   odotdev.blogbitix.temporal.activities.DefaultOrderActivities Order create (orderId: 196969, listingId: 1, quantity; 3)
2026-05-07 20:19:26,513  INFO   odotdev.blogbitix.temporal.activities.DefaultFraudActivities Fraud decision (orderId: 196969, authorizeId: 848778, decision: APPROVE)
2026-05-07 20:19:26,520  INFO   odotdev.blogbitix.temporal.activities.DefaultOrderActivities Order approve (orderId: 196969)
2026-05-07 20:19:31,252  INFO                               io.temporal.worker.WorkerFactory shutdown: WorkerFactory{identity=691@localhost}
System.out

La línea de comandos de Temporal permite iniciar workflows, actividades y enviar mensajes además de consultar el estado de los workflows. Temporal tiene una interfaz gráfica que permite visualizar y ver el estado de la ejecución de cada proceso, con los datos de entrada y salida de las actividades así como los tiempos de inicio y fin. Esta observabilidad es una característica que no tiene un sistema distribuido que emplea coreografía cuando hay que examinar en que estado está el proceso por alguna inconsistencia de datos.

Worflow en Temporal Worflow en Temporal

Worflow en Temporal

Finalmente, es posible construir teses unitarios para el proceso.

Videos de introducción

Terminal

El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando siguiente comando:
./gradlew run


Comparte el artículo: