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:
- Determinism is Non-Negotiable: Never use
System.currentTimeMillis()or random number generators inside the Workflow method. UseWorkflow.currentTimeMillis()instead. The workflow logic replays from scratch on every invocation to ensure consistency. - 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.
- 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.