Workflow Automation

Mastering Long-Running Workflows: Java-Specific Patterns with Temporal.io

In the world of modern microservices architecture, handling long-running business processes is one of the most persistent challenges. Whether it is an order fulfillment pipeline, a complex approval workflow, or an IoT device management sequence, traditional synchronous request-response models often fall short. They struggle with timeouts, state persistence, and resilience against transient failures.

Enter Temporal.io, an open-source platform for reliable, distributed applications. While Temporal supports multiple languages, the Java SDK offers unique advantages for enterprise developers, including strong typing, integration with familiar ecosystems like Spring Boot, and robust tooling. This post explores Java-specific patterns for building resilient, long-running workflows using Temporal.

The Core Abstraction: Workflow vs. Activity

Before diving into code, it is crucial to understand the mental model shift Temporal introduces. In traditional service-oriented architectures, you write a sequence of service calls. In Temporal, you define a Workflow (the state machine) and Activities (the side-effecting functions). The Workflow logic is deterministic and replay-safe, while Activities handle I/O, such as database updates or API calls.

The key Java-specific pattern here is separating logic. Your Workflow methods should be lightweight, containing only control flow. All heavy lifting goes into Activities.

Implementing a Resilient Approval Workflow

Let’s look at a practical example: a procurement approval process. This workflow requires waiting for user input (approval/rejection) while ensuring the system remains responsive and stateful.

First, define the Activity interface. This represents the external action.

public interface ProcurementActivities {
    @ActivityMethod
    void submitOrderForApproval(String orderId, double amount);
    
    @ActivityMethod
    void processApprovedOrder(String orderId);
    
    @ActivityMethod
    void rejectOrder(String orderId, String reason);
}

Now, implement the Workflow. In Java, you extend WorkflowInterface and use Workflow.newChildWorkflowOptions() for nested workflows or Workflow.await() for signals. A signal allows the workflow to pause and wait for external input.

@WorkflowInterface
public interface ProcurementWorkflow {
    @WorkflowMethod
    void processOrder(String orderId, double amount);

    @SignalMethod
    void approve(String orderId);

    @SignalMethod
    void reject(String orderId, String reason);
}

@WorkflowImplementation
public class ProcurementWorkflowImpl implements ProcurementWorkflow {

    private final ProcurementActivities activities = Workflow.newActivityStub(
        ProcurementActivities.class,
        Workflow.newActivityOptions(
            ActivityOptions.newBuilder()
                .setScheduleToCloseTimeout(Duration.ofSeconds(30))
                .build()
        )
    );

    private volatile boolean approved = false;
    private String rejectionReason = null;

    @Override
    public void processOrder(String orderId, double amount) {
        activities.submitOrderForApproval(orderId, amount);

        // Wait indefinitely for a signal
        Workflow.await(() -> approved || rejectionReason != null);

        if (approved) {
            activities.processApprovedOrder(orderId);
        } else {
            activities.rejectOrder(orderId, rejectionReason);
        }
    }

    @Override
    public void approve(String orderId) {
        this.approved = true;
    }

    @Override
    public void reject(String orderId, String reason) {
        this.rejectionReason = reason;
    }
}

Key Java Patterns for Production

When implementing these patterns in Java, keep three best practices in mind:

  1. Determinism is Non-Negotiable: Never use System.currentTimeMillis() or random number generators inside the Workflow method. Use Workflow.currentTimeMillis() instead. The workflow logic replays from scratch on every invocation to ensure consistency.
  2. Thread Safety: Since workflows can replay, shared state must be managed carefully. The volatile keyword in the example above ensures visibility across threads, but generally, you should treat Workflow state as immutable or strictly controlled.
  3. Exception Handling: Let Activities throw exceptions for failures. The Workflow can catch these and implement retry logic or fallback paths, keeping your business logic clean and focused.

Conclusion

Java developers have a powerful toolkit for building long-running business processes with Temporal.io. By decoupling state management from I/O and leveraging deterministic replay, you can build systems that are not only resilient but also observable and debuggable. As your business processes grow in complexity, Temporal’s Java SDK provides the structural integrity needed to scale without sacrificing reliability. Start small, define clear Activities, and let the framework handle the orchestration.

Share: