Simply Order (Part 2) — Designing and Implementing the Saga Workflow with Temporal



This content originally appeared on DEV Community and was authored by Mohamed Hassan

This is the second lesson in our series. In the first lesson, we defined the problem of distributed microservices in our hypothetical company Simply Order. We explored possible solutions for handling distributed transactions in microservices, and discussed why Two-Phase Commit (2PC) is not suitable for microservices and why the Saga pattern can solve these problems.

In this lesson, we move from concepts to implementation. We will design and implement the first version of our Saga workflow using Temporal as the orchestrator.

Before we code: How to think about a saga?

1) Think about the business transaction (i.e. Place Order)

The transaction will be as follows: create order → reserve inventory → authorize payment → completed

2) Draw the state machine of our transaction

To be able to implement the transaction, we need to draw the state machine — i.e. all the states that our entity will go through.

In our case, we can think of an order with these states:

OPEN-> PENDING -> INVENTORY_RESERVED -> PAYMENT_AUTHORIZED -> COMPLETED

In case of errors, the status will be one of these:

INVENTORY_FAILED, PAYMENT_FAILED, FAILED_UNKNOWN

3) Why Orchestration Saga?

⚠ By default, we should consider Choreography because it is simpler: services just emit and listen to events. This works well for small, linear flows with few participants (see Chris Richardson, Microservices Patterns, and Sam Newman, Building Microservices).

But as soon as the flow becomes longer, more complex, or involves branching, choreography can easily turn into a spaghetti of events, making it hard to trace and evolve the process.

In the initial phase of our order service — which is relatively simple — choreography could be a good choice. But with our planned extension of the order service (as we’ll see in the next lessons), we opt for Orchestration Saga.

We could implement our own saga orchestrator, but then we would need to handle workflow state, communication between the orchestrator and microservices, reliability, non-blocking execution, and visibility into workflow progress. All of this distracts us from focusing on business logic, which is what we as developers should prioritize.

Here is where Temporal comes in as a solution…

What is Temporal

In a nutshell, Temporal is an open-source workflow orchestration platform that lets you write reliable, long-running processes in code. It handles retries, state, timeouts, and compensations automatically, so you can focus on business logic instead of building reliability infrastructure.

👉 Note: Temporal is not limited to sagas — it’s a general-purpose workflow platform. Saga orchestration is just one of many patterns you can implement with it.

In this lesson, we will focus on how to configure Temporal in our code and explore its core components.

Dive to Implmentation

The code for this project can be found in this repository:
https://github.com/hassan314159/simply-order

Since this repository is continuously updated, the code specific to this lesson can be found in the branch initial_order_saga. Start with:

git checkout initial_order_saga

Order Workflow definition

Let’s jump to the implementation of our Saga workflow in OrderWorkflowImpl.java.

    public void placeOrder(Input in) {
        String sagaId = Workflow.getInfo().getWorkflowId();
        Saga saga = new Saga(new Saga.Options.Builder()
                .setContinueWithError(true) // collect/continue if a compensation fails
                .build());
        try {
            // 1) Update status of Order to PENDING
            act.updateOrderStatus(in.orderId(), Order.Status.PENDING);

            // 2) reserve the inventory (calling inventory service)
            UUID reservationId = act.reserveInventory(in.orderId(), sagaId, in.request().items());
            saga.addCompensation(() -> act.releaseInventoryIfAny(in.orderId(), reservationId));

            // 3) Update status of Order to INVENTORY_RESERVED
            act.updateOrderStatus(in.orderId(), Order.Status.INVENTORY_RESERVED);

            // 4) authorize the payment (calling payment service)
            UUID authId = act.authorizePayment(in.orderId(), sagaId, in.total());
            saga.addCompensation(() -> act.voidPaymentIfAny(in.orderId(), authId));

            // 5) Update status of Order to PAYMENT_AUTHORIZED
            act.updateOrderStatus(in.orderId(), Order.Status.PAYMENT_AUTHORIZED);

            // 6) Complete our saga by updating Order Status to COMPLETED
            act.updateOrderStatus(in.orderId(), Order.Status.COMPLETED);
        } catch (Exception e) {
            try {
                saga.compensate();     // runs: voidPayment → releaseInventory
            } finally {
                act.updateOrderStatus(in.orderId(), classifyError(e)); // precise status
            }
            throw e;
        }
    }

One of the great features of Temporal is the separation of concerns. In this class, we define our saga flow independently of how the steps are implemented:

  1. Update the order status to PENDING
  2. Call the Inventory service to reserve inventory, and register a compensation step in case an error happens.
  3. Update the order status to INVENTORY_RESERVED
  4. Call the Payment service to authorize the payment, and again register a compensation step.
  5. Update the order status to PAYMENT_AUTHORIZED
  6. Finally, complete the saga by updating the order status to COMPLETED

Defining the workflow steps and compensation logic is straightforward.
We define the steps in the required business order. If any step needs compensation, we register the compensation after the step succeeds, so that if an error occurs later, compensation runs only for the steps that actually completed before the exception point.

Now let’s look at a snippet of the reserveInventory implementation from OrderActivitiesImpl.java.

var req = new Req(orderId, items.stream().map(i -> new Item(i.sku(), i.qty())).toList());
        var headers = new HttpHeaders();
        headers.add("Idempotency-Key", sagaId + ":reserve");
        var res = http.exchange(URI.create(inventoryBase + "/inventory/reservations"), HttpMethod.POST, new HttpEntity<>(req, headers), Res.class);

        if (res.getStatusCode().value() == 209) {
            throw ApplicationFailure.newNonRetryableFailure("" , "InventoryNotSufficient");
        }

The code may look like it’s blocking — just a synchronous HTTP call to the Inventory service.
However, behind the scenes, each activity (act.) is executed by a Temporal Worker, which is a process that runs in a separate worker threads. This means it’s non-blocking from the workflow’s perspective.

We can also define timeout and retry behavior, as shown in our constructor.

 var retry = RetryOptions.newBuilder().setMaximumAttempts(5).setBackoffCoefficient(2.0).setInitialInterval(Duration.ofSeconds(1)).build();
        var opts = ActivityOptions.newBuilder().setStartToCloseTimeout(Duration.ofMinutes(2)).setRetryOptions(retry).build();

Here, we defined a single config for all activities, but in practice, each activity type can have its own configuration.

For example, in this lesson we set a timeout of 2 minutes. In reality, Temporal allows activities to run indefinitely with automatic retries until they succeed — unless you explicitly configure a timeout or retry policy.

Quick Overview of Temporal Core Components

Before moving on, let’s quickly recap the core components of Temporal (we’ll dive deeper into how they interact in a following lesson):

  • Workflow code: Your orchestration logic (deterministic, no external I/O).
  • Activity code: Your side effects (call services/DBs/APIs).
  • Worker: The process that runs workflow code and activity code. It polls a Task Queue.
  • Temporal Server: Stores the event history, hands out tasks, tracks retries/timeouts.
  • Task Queue: A named queue that decouples the server from workers.
  • Client: Starts workflows, sends Signals, reads Queries.
  • Persistence: DB/storage where the server saves workflow history/state.

Continue Implementation…

Let’s go one step back and see how we can set up our project with Temporal and define its core components.

First, we need to import the Temporal SDK dependency:

<dependency>
    <groupId>io.temporal</groupId>
    <artifactId>temporal-sdk</artifactId>
    <version>${temporal.version}</version>
</dependency>

Then, let’s define the Client and Worker beans. You can find this in TemporalConfig.java.

 @Bean
    WorkflowServiceStubs service(@Value("${app.temporal.address}") String address) {
        return WorkflowServiceStubs.newServiceStubs(
                io.temporal.serviceclient.WorkflowServiceStubsOptions.newBuilder()
                        .setTarget(address).build());
    }

    @Bean
    WorkflowClient workflowClient(WorkflowServiceStubs service) {
        return WorkflowClient.newInstance(service);
    }

    @Bean
    WorkerFactory workerFactory(WorkflowClient client) {
        return WorkerFactory.newInstance(client);
    }

    @Bean
    Worker worker(WorkerFactory factory, OrderActivities activities) {
        Worker w = factory.newWorker("order-task-queue");
        w.registerWorkflowImplementationTypes(OrderWorkflowImpl.class);
        w.registerActivitiesImplementations(activities);
        factory.start();
        return w;
    }

We’ve seen the workflow code, but how does our app identify it as a Temporal Workflow?
Temporal provides two annotations: @WorkflowInterface and @WorkflowMethod.
We already saw the implementation of the placeOrder method.

@WorkflowInterface
public interface OrderWorkflow {
    record Input(UUID orderId, UUID customerId, BigDecimal total, CreateOrderRequest request) {
        public static Input from(Order order, CreateOrderRequest req) {
            return new Input(order.getId(), order.getCustomerId(), order.getTotal(), req);
        }
    }

    @WorkflowMethod
    void placeOrder(Input input);
}

Lastly, Temporal also needs to know the interface that defines the activities, so that:

  • The Worker can register those methods as activities.
  • The Server can schedule activity tasks and match them to the correct code running in the Worker.

and this is done simple by annotate the interface with @ActivityInterface as in OrderActivities.java

@ActivityInterface
public interface OrderActivities {
    void updateOrderStatus(UUID orderId, Order.Status status);

    UUID reserveInventory(UUID orderId, String sagaId, List<CreateOrderRequest.Item> items);

    UUID authorizePayment(UUID orderId, String sagaId, BigDecimal total);

    void voidPaymentIfAny(UUID orderId, UUID paymentId);

    void releaseInventoryIfAny(UUID orderId, UUID reservationId);
}

Wrapping up

rapping up

In this lesson, we turned the theory of Sagas into a working Order Workflow with Temporal — defining states, compensations, and error handling. This is just our first step, and there’s more to come as we add cancellation, shipment, and other real-world features.

👉 Follow along for the next parts of the series, and feel free to drop your questions or share your own Saga experiences in the comments — I’d love to hear your thoughts!


This content originally appeared on DEV Community and was authored by Mohamed Hassan