Building Reliable Distributed Applications: A Guide to Temporal.io Workflow Engine in Java

Article

In distributed systems, failures are not the exception—they are the norm. Network timeouts, service crashes, and partial failures make building reliable applications incredibly challenging. Temporal.io is an open-source, stateful, and durable workflow engine that lets you write complex, fault-tolerant business logic in plain Java code without worrying about the underlying infrastructure complexities.

This article will explore Temporal's core concepts, demonstrate how to implement reliable workflows and activities in Java, and show why it's becoming an essential tool for mission-critical distributed systems.


What is Temporal.io?

Temporal is a developer-friendly, stateful workflow engine that provides durable execution for your application code. It ensures that your workflow logic progresses to completion, surviving process failures, infrastructure outages, and code deployments.

Key Characteristics:

  • Durable Execution: Workflows can run for days, months, or even years, surviving any kind of failure.
  • State Management: Automatic persistence of workflow state, including local variables and thread stacks.
  • Built-in Reliability: Automatic retries, timeouts, and error handling for all operations.
  • Observability: Built-in visibility into workflow execution history and state.

Core Concepts: Workflows and Activities

1. Workflows

  • The orchestration logic that defines the business process.
  • Must be deterministic: cannot use random functions, current time, or external APIs directly.
  • Can sleep for days, wait for signals, and execute activities.
  • Represented as Java interfaces and implementations.

2. Activities

  • The non-deterministic operations that perform actual work (API calls, database operations, etc.).
  • Can contain any code, including network calls and I/O operations.
  • Automatically retried with configurable policies.

3. Workers

  • Java programs that host workflow and activity implementations.
  • Poll the Temporal server for tasks and execute them.

Setting Up Temporal with Java

1. Maven Dependencies

<properties>
<temporal.version>1.23.3</temporal.version>
</properties>
<dependencies>
<dependency>
<groupId>io.temporal</groupId>
<artifactId>temporal-sdk</artifactId>
<version>${temporal.version}</version>
</dependency>
<!-- For testing -->
<dependency>
<groupId>io.temporal</groupId>
<artifactId>temporal-testing</artifactId>
<version>${temporal.version}</version>
<scope>test</scope>
</dependency>
</dependencies>

2. Running Temporal Server

The easiest way to run Temporal locally is via Docker Compose:

# docker-compose.yml
version: '3.8'
services:
temporal:
image: temporalio/auto-setup:1.23.3
ports:
- "7233:7233"
environment:
- DB=postgresql
- DB_PORT=5432
- POSTGRES_USER=temporal
- POSTGRES_PWD=temporal
- POSTGRES_SEEDS=postgresql
depends_on:
- postgresql
postgresql:
image: postgres:14
environment:
- POSTGRES_USER=temporal
- POSTGRES_PASSWORD=temporal
- POSTGRES_DB=temporal

Implementing a Complete Example: Order Processing Workflow

Let's build a reliable order processing system that handles payment, inventory reservation, and notification.

1. Define Workflow Interface

@WorkflowInterface
public interface OrderProcessingWorkflow {
@WorkflowMethod
String processOrder(Order order);
// Signal to allow external updates (e.g., manual approval)
@SignalMethod
void approveOrder();
@SignalMethod
void cancelOrder(String reason);
// Query to check current workflow state
@QueryMethod
OrderStatus getOrderStatus();
}
// Data classes
public class Order {
private String orderId;
private String customerId;
private BigDecimal amount;
private List<OrderItem> items;
// ... constructors, getters
}
public enum OrderStatus {
CREATED, PAYMENT_PENDING, PAYMENT_PROCESSING, 
INVENTORY_RESERVING, COMPLETED, CANCELLED
}

2. Implement Activities

// Activity Interface
public interface OrderActivities {
@ActivityMethod(name = "processPayment")
PaymentResult processPayment(PaymentRequest request);
@ActivityMethod
InventoryResult reserveInventory(InventoryRequest request);
@ActivityMethod
void sendConfirmationEmail(String orderId, String customerEmail);
@ActivityMethod
void refundPayment(String paymentId);
@ActivityMethod  
void releaseInventory(String orderId);
}
// Activity Implementation
public class OrderActivitiesImpl implements OrderActivities {
@Override
public PaymentResult processPayment(PaymentRequest request) {
// Simulate payment processing - this could call Stripe, PayPal, etc.
try {
// This is where non-deterministic operations happen
// Network calls, database operations, etc.
System.out.println("Processing payment for order: " + request.getOrderId());
// Simulate API call
Thread.sleep(1000);
// In real implementation, this would call a payment gateway
if (request.getAmount().compareTo(new BigDecimal("1000")) > 0) {
// Large payments require manual review
return new PaymentResult("PENDING_REVIEW", null);
}
return new PaymentResult("SUCCESS", "pay_" + System.currentTimeMillis());
} catch (InterruptedException e) {
throw Activity.wrap(new RuntimeException("Payment processing interrupted"));
}
}
@Override
public InventoryResult reserveInventory(InventoryRequest request) {
System.out.println("Reserving inventory for order: " + request.getOrderId());
// Simulate inventory service call
// This could throw exceptions, which Temporal will handle with retries
return new InventoryResult(true, "inventory_reserved");
}
@Override
public void sendConfirmationEmail(String orderId, String customerEmail) {
System.out.println("Sending confirmation email for order: " + orderId);
// Email sending logic with retries
}
@Override
public void refundPayment(String paymentId) {
System.out.println("Refunding payment: " + paymentId);
// Refund logic
}
@Override
public void releaseInventory(String orderId) {
System.out.println("Releasing inventory for order: " + orderId);
// Inventory release logic
}
}

3. Implement the Workflow

public class OrderProcessingWorkflowImpl implements OrderProcessingWorkflow {
private final OrderActivities activities = 
Workflow.newActivityStub(OrderActivities.class,
ActivityOptions.newBuilder()
.setStartToCloseTimeout(Duration.ofSeconds(10))
.setRetryOptions(RetryOptions.newBuilder()
.setInitialInterval(Duration.ofSeconds(1))
.setMaximumInterval(Duration.ofSeconds(100))
.setBackoffCoefficient(2.0)
.setMaximumAttempts(3)
.build())
.build());
private OrderStatus status = OrderStatus.CREATED;
private boolean approved = false;
private boolean cancelled = false;
private String cancellationReason;
@Override
public String processOrder(Order order) {
try {
// Step 1: Process payment
status = OrderStatus.PAYMENT_PROCESSING;
PaymentResult paymentResult = activities.processPayment(
new PaymentRequest(order.getOrderId(), order.getAmount(), order.getCustomerId()));
// Handle payment that requires manual review
if ("PENDING_REVIEW".equals(paymentResult.getStatus())) {
status = OrderStatus.PAYMENT_PENDING;
// Wait for manual approval signal (can wait for days!)
Workflow.await(Duration.ofDays(7), () -> approved || cancelled);
if (cancelled) {
return handleCancellation("Order cancelled during payment review: " + cancellationReason);
}
if (!approved) {
return handleCancellation("Payment not approved within 7 days");
}
// Retry payment after approval
paymentResult = activities.processPayment(
new PaymentRequest(order.getOrderId(), order.getAmount(), order.getCustomerId()));
}
if (!"SUCCESS".equals(paymentResult.getStatus())) {
return handleCancellation("Payment failed: " + paymentResult.getStatus());
}
// Step 2: Reserve inventory
status = OrderStatus.INVENTORY_RESERVING;
InventoryResult inventoryResult = activities.reserveInventory(
new InventoryRequest(order.getOrderId(), order.getItems()));
if (!inventoryResult.isSuccess()) {
// Compensation: refund payment since inventory failed
activities.refundPayment(paymentResult.getPaymentId());
return handleCancellation("Inventory reservation failed");
}
// Step 3: Send confirmation
activities.sendConfirmationEmail(order.getOrderId(), order.getCustomerId() + "@example.com");
status = OrderStatus.COMPLETED;
return "Order processed successfully. Payment: " + paymentResult.getPaymentId();
} catch (Exception e) {
// Any unhandled exception will cause the workflow to retry from the beginning
// or you can implement compensation logic
return handleCancellation("Unexpected error: " + e.getMessage());
}
}
private String handleCancellation(String reason) {
status = OrderStatus.CANCELLED;
// In a real implementation, you would implement proper compensation/saga pattern
System.out.println("Order cancelled: " + reason);
return "FAILED: " + reason;
}
@Override
public void approveOrder() {
this.approved = true;
}
@Override
public void cancelOrder(String reason) {
this.cancelled = true;
this.cancellationReason = reason;
}
@Override
public OrderStatus getOrderStatus() {
return status;
}
}

4. Worker Implementation

public class OrderWorker {
public static void main(String[] args) {
// Create client
WorkflowServiceStubs service = WorkflowServiceStubs.newLocalServiceStubs();
WorkflowClient client = WorkflowClient.newInstance(service);
// Create worker
WorkerFactory factory = WorkerFactory.newInstance(client);
Worker worker = factory.newWorker("ORDER_TASK_QUEUE");
// Register workflow and activity implementations
worker.registerWorkflowImplementationTypes(OrderProcessingWorkflowImpl.class);
worker.registerActivitiesImplementations(new OrderActivitiesImpl());
// Start polling
factory.start();
System.out.println("Order Worker started for task queue: ORDER_TASK_QUEUE");
}
}

5. Starting Workflow Executions

public class OrderStarter {
public static void main(String[] args) {
WorkflowServiceStubs service = WorkflowServiceStubs.newLocalServiceStubs();
WorkflowClient client = WorkflowClient.newInstance(service);
// Create workflow stub
OrderProcessingWorkflow workflow = 
client.newWorkflowStub(OrderProcessingWorkflow.class,
WorkflowOptions.newBuilder()
.setTaskQueue("ORDER_TASK_QUEUE")
.setWorkflowId("order-" + System.currentTimeMillis())
.setWorkflowExecutionTimeout(Duration.ofHours(24))
.build());
// Create order
Order order = new Order("order-123", "customer-456", 
new BigDecimal("149.99"), List.of(new OrderItem("item1", 2)));
// Start workflow asynchronously
WorkflowClient.start(workflow::processOrder, order);
System.out.println("Workflow started for order: order-123");
// You can later signal the workflow
// workflow.approveOrder();
// workflow.cancelOrder("Customer requested cancellation");
}
}

Testing Temporal Workflows

Temporal provides excellent testing support:

public class OrderProcessingWorkflowTest {
private TestWorkflowEnvironment testEnv;
private Worker worker;
private WorkflowClient client;
@Before
public void setUp() {
testEnv = TestWorkflowEnvironment.newInstance();
worker = testEnv.newWorker("ORDER_TASK_QUEUE");
worker.registerWorkflowImplementationTypes(OrderProcessingWorkflowImpl.class);
// Mock activities
OrderActivities mockActivities = mock(OrderActivities.class);
when(mockActivities.processPayment(any()))
.thenReturn(new PaymentResult("SUCCESS", "pay_123"));
when(mockActivities.reserveInventory(any()))
.thenReturn(new InventoryResult(true, "reserved"));
worker.registerActivitiesImplementations(mockActivities);
testEnv.start();
client = testEnv.getWorkflowClient();
}
@Test
public void testSuccessfulOrder() {
OrderProcessingWorkflow workflow = 
client.newWorkflowStub(OrderProcessingWorkflow.class,
WorkflowOptions.newBuilder()
.setTaskQueue("ORDER_TASK_QUEUE")
.build());
Order order = new Order("test-order", "test-customer", 
new BigDecimal("99.99"), List.of());
String result = workflow.processOrder(order);
assertEquals("Order processed successfully. Payment: pay_123", result);
}
}

Key Benefits of Temporal

  1. Durability: Workflows survive process crashes, infrastructure failures, and code deployments.
  2. Built-in Reliability: Automatic retries, timeouts, and error handling.
  3. Visibility: Complete audit trail of workflow execution.
  4. Developer Experience: Write business logic in plain Java without distributed systems complexity.
  5. Scalability: Horizontal scaling of workers.

When to Use Temporal

  • Business Process Orchestration: Order processing, onboarding flows, loan applications.
  • Saga Pattern Implementation: Distributed transactions across multiple services.
  • Scheduled Jobs: Complex cron jobs that require reliability.
  • Data Pipeline Orchestration: Multi-step ETL processes.
  • Human-in-the-Loop Workflows: Processes requiring manual approval steps.

Conclusion

Temporal.io represents a paradigm shift in how we build reliable distributed applications. By providing durable execution for regular Java code, it eliminates entire classes of distributed systems problems. The combination of workflow durability, built-in reliability patterns, and excellent developer experience makes Temporal an indispensable tool for building mission-critical business processes.

For Java teams building complex distributed systems, Temporal offers the reliability of enterprise workflow engines with the developer-friendly approach of modern cloud-native tools. It allows you to focus on business logic while the platform handles the complexities of distributed state management and fault tolerance.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper