DEV Community

Cover image for Simply Order (Part 3) — Linking It All Together: Connecting Services and Watching Temporal in Action
Mohamed Hassan
Mohamed Hassan

Posted on

Simply Order (Part 3) — Linking It All Together: Connecting Services and Watching Temporal in Action

This is the Third article in our series, that we design a simple order
This is the third article in our series, where we design a simple order solution for a hypothetical company called “Simply Order”. The company expects high traffic and needs a resilient, scalable, and distributed order system.

In previous lecture Designing and Implementing the Saga Workflow, we designed and implemented the Temporal workflow. In this lecture, we’ll build the skeleton for the three participating services, connect them together, and run the application using Docker Compose.

We’ll start with in-memory data only. In the next lecture, we’ll add a persistence layer, discuss the challenges that come with it, and explore possible solutions and their trade-offs. So stay tuned—let’s dive into the code for our services:

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 connecting_and_running_app. Start with:

git checkout connecting_and_running_app
Enter fullscreen mode Exit fullscreen mode

Order Service

The Order Service is our entry point—the place where we create orders and start the workflow.

Controller

@PostMapping
public ResponseEntity<CreateOrderResponse> create(@RequestBody CreateOrderRequest req) {
    UUID id = orderService.createOrder(req);
    return ResponseEntity.status(201).body(new CreateOrderResponse(id, orderService.findOrderById(id).getStatus().name()));
}

@GetMapping("/{id}")
public ResponseEntity<?> get(@PathVariable UUID id) {
    return orderService.findOrderById(id) != null ? ResponseEntity.ok(orderService.findOrderById(id)) :
           ResponseEntity.notFound().build();
}
Enter fullscreen mode Exit fullscreen mode

It exposes two endpoints: one to create an order and another to retrieve an order with its status.

Service

When a new order is created, the service builds an order object with status OPEN and stores it in a Map that acts as our in-memory database. It then starts the workflow code we defined in the previous lecture, as shown below:

 public UUID createOrder(CreateOrderRequest request){
    ...

    Order order = new Order(orderId, request.customerId(), Order.Status.OPEN, total, items);
    // simple map acts as in in-memory store
    orders.put(orderId, order);

    // start oder creation saga ** WHEN INTRODUCE DB WILL BE UPDATED TO BE STARTED BY OUTBOX RELAY
    OrderWorkflow wf = client.newWorkflowStub(
            OrderWorkflow.class,
            WorkflowOptions.newBuilder()
                    .setTaskQueue("order-task-queue")
                    .setWorkflowId("order-" + orderId) // This is saga id
                    .build());

    WorkflowClient.start(wf::placeOrder, OrderWorkflow.Input.from(order, request));
    return orderId;

}
Enter fullscreen mode Exit fullscreen mode

Inventory Service

The Inventory Service is very simple—it accepts a request and returns an OK response so the Saga can continue.

We added a temporary condition: if the number of items is greater than 3, the service responds with 209, indicating that the inventory does not have enough items. This allows our Saga to roll back, i.e., apply the compensation operations.

But what if the service is unreachable? Will the Saga fail on its request?
Short answer: No. This depends on the activity retry configuration we defined in the previous lecture.

We’ve also provided an endpoint that Temporal workers will call to apply the compensation. /reservations/{reservationsId}/release

@PostMapping("/reservations")
public ResponseEntity<ReservationsResponse> create(@RequestBody ReservationsRequest req) {
    if(req.items().size() < 3){
        LOG.info("Items reserved successfully");
        return ResponseEntity.ok(new ReservationsResponse(UUID.randomUUID()));
    }else {
        LOG.info("Items could not be reserved");
        return ResponseEntity.status(209).build();
    }
}

@PostMapping("/reservations/{reservationsId}/release")
public ResponseEntity<?> get(@PathVariable UUID reservationsId) {
    LOG.info("Items released for reservation: {}", reservationsId);
    return ResponseEntity.noContent().build();
}
Enter fullscreen mode Exit fullscreen mode

Payment Service

Like the Inventory Service, the Payment Service authorizes requests with trivial logic: if the order cost is less than 500, it authorizes the payment; if it’s 500 or greater, it rejects it and returns 209 as a rollback signal so the Saga can apply compensation.

@PostMapping("/authorize")
public ResponseEntity<PaymentAuthorizeResponse> create(@RequestBody PaymentAuthorizeRequest req) {
    if(req.amount().compareTo(BigDecimal.valueOf(500)) < 0){
        LOG.info("Payment processed successfully");
        return ResponseEntity.ok(new PaymentAuthorizeResponse(UUID.randomUUID(), false));
    }else {
        LOG.info("Payment Failed");
        return ResponseEntity.status(209).build();
    }
}

@PostMapping("/{paymentId}/void")
public ResponseEntity<?> get(@PathVariable UUID authId) {
    LOG.info("Payment refunded: {}", authId);
    return ResponseEntity.noContent().build();
}
Enter fullscreen mode Exit fullscreen mode

Run the application

We’ve provided a Docker Compose setup that builds and starts the services, Temporal Server, and Temporal UI. Temporal UI is a powerful tool that allows us to trace the progress of our workflows.

If Docker Compose isn’t installed in your environment, install it first. Then, from the repository’s root directory, navigate to the folder containing the Docker Compose file and run the services:

cd ./deploy/local
docker compose up
Enter fullscreen mode Exit fullscreen mode

To check the running services, just run docker-compose ps. You should see something like:

services staus

First Order Creation

One of the best auxiliary features of Temporal is the Temporal UI — an interface that lets us trace our workflows.

Let’s open this URL in our browser: localhost:8080

Temporal UI, Zero Workflow

For now, no workflows have been created.
Let’s start a transaction and see how we can trace it.

Let’s create our first order:

curl --location 'localhost:8081/orders' \
--header 'Content-Type: application/json' \
--data '{
    "customerId": "123e4567-e89b-12d3-a456-426614174000",
    "items": [
      {
        "sku": "ABC-001",
        "qty": 1,
        "price": 19.99
      },
      {
        "sku": "XYZ-123",
        "qty": 1,
        "price": 12.95
      }
    ]
  }'
Enter fullscreen mode Exit fullscreen mode

The response should look like this:

{
    "orderId": "71ed318d-10ea-4371-af34-1581dc315dde",
    "status": "OPEN"
}
Enter fullscreen mode Exit fullscreen mode

We’ve created an order, and its status is OPEN. Let’s check its status for now:

Let’s call:

curl --location 'localhost:8081/orders/71ed318d-10ea-4371-af34-1581dc315dde'
Enter fullscreen mode Exit fullscreen mode

Here, 71ed318d-10ea-4371-af34-1581dc315dde is the order ID you got when creating the order. You should use the UUID generated by your own code.

{
    "id": "71ed318d-10ea-4371-af34-1581dc315dde",
    "customerId": "123e4567-e89b-12d3-a456-426614174000",
    "status": "COMPLETED",
    "total": 32.94,
    "items": [
        {
            "sku": "ABC-001",
            "qty": 1,
            "price": 19.99
        },
        {
            "sku": "XYZ-123",
            "qty": 1,
            "price": 12.95
        }
    ]
}
Enter fullscreen mode Exit fullscreen mode

We can see that the status is Completed — but what happened?
This is where the Temporal UI comes in. Let’s open its URL again localhost:8080 and click on the workflow.
You can identify it as order-<UUID>

Now we can see the timeline of our order’s workflow, as shown here:

Order Creation Workflow

One Service is down

Let’s assume one service is down — for example, the payment service. How can Temporal handle this?

In our branch, we’ve updated the retry interval of the workflow activity to 200 seconds so we can debug the workflow while the service is down.

var retry = RetryOptions.newBuilder().setMaximumAttempts(5).setBackoffCoefficient(2.0).setInitialInterval(Duration.ofSeconds(200)).build();
Enter fullscreen mode Exit fullscreen mode

To simulate a service being down, just stop the payment service.

docker compose stop payment-service
Enter fullscreen mode Exit fullscreen mode

Now, let’s create another order and check its status:

curl --location 'localhost:8081/orders' \
--header 'Content-Type: application/json' \
--data '{
    "customerId": "123e4567-e89b-12d3-a456-426614174000",
    "items": [
      {
        "sku": "ABC-001",
        "qty": 1,
        "price": 19.99
      },
      {
        "sku": "XYZ-123",
        "qty": 1,
        "price": 12.95
      }
    ]
  }'

Response: 
{
    "orderId": "9cd35ac0-6973-4881-b68d-a37dbb1a2bff",
    "status": "OPEN"
}
Enter fullscreen mode Exit fullscreen mode

Let’s check the status of the order right now:

curl --location 'localhost:8081/orders/9cd35ac0-6973-4881-b68d-a37dbb1a2bff'

Response
{
    "id": "9cd35ac0-6973-4881-b68d-a37dbb1a2bff",
    "customerId": "123e4567-e89b-12d3-a456-426614174000",
    "status": "INVENTORY_RESERVED",
    "total": 32.94,
    "items": [
        {
            "sku": "ABC-001",
            "qty": 1,
            "price": 19.99
        },
        {
            "sku": "XYZ-123",
            "qty": 1,
            "price": 12.95
        }
    ]

Enter fullscreen mode Exit fullscreen mode

We can see that the status of the order is INVENTORY_RESERVED, which is expected. Since the payment service is down, Temporal activities are retrying calls to the payment service. According to our configuration, if the service becomes healthy again before 5 retries, our workflow can continue.

If we check the Temporal UI, we’ll see its status as Running:
Running Workflow

Let’s start the service again:

docker compose start payment-service
Enter fullscreen mode Exit fullscreen mode

Let’s check the status.

curl --location 'localhost:8081/orders/9cd35ac0-6973-4881-b68d-a37dbb1a2bff'

Response
{
    "id": "9cd35ac0-6973-4881-b68d-a37dbb1a2bff",
    "customerId": "123e4567-e89b-12d3-a456-426614174000",
    "status": "COMPLETED",
    "total": 32.94,
    "items": [
        {
            "sku": "ABC-001",
            "qty": 1,
            "price": 19.99
        },
        {
            "sku": "XYZ-123",
            "qty": 1,
            "price": 12.95
        }
    ]
}
Enter fullscreen mode Exit fullscreen mode

now it is COMPLETED — whoa! 🎉

To trace the timeline of what happened, we can check the Temporal UI:

Temporal History

From the timeline above, we can see when the service was down (light green) and when it became healthy again and succeeded.

Home Work

Try stopping the payment service and creating an order, but this time wait longer — until the order workflow fails — and then check the status…

Wrapping Up

In this session, we continued our example by running the app and exploring how Temporal helps us trace and recover workflows — even when a service goes down. We saw how the Temporal UI gives us full visibility into the workflow timeline and retries.

In the next lesson, we’ll take our example one step further and add persistence, so our workflows can survive restarts and keep their state safely. 🚀

Top comments (0)