Temporal Java
Temporal changed how I think about distributed systems. Before Temporal, building a multi-step workflow that survives server crashes, network partitions, and deployment restarts meant writing a lot of state machine code, database checkpoints, and retry logic by hand. With Temporal, you write a normal Java method and the platform handles durability.
But Temporal also has a learning curve that catches people. Here's how I structure Temporal Java applications and the mistakes I've learned to avoid.
Architecture Overview
A Temporal Java application has three distinct layers, and keeping them separate matters more than you might think.
Worker Layer
The worker is the JVM process that polls Temporal Server for tasks and executes them. You register your workflow and activity implementations here. The critical thing: workers are stateless. You can run 10 of them, kill 5, and nothing breaks. Temporal handles the task routing.
WorkerFactory factory = WorkerFactory.newInstance(client);
Worker worker = factory.newWorker("order-processing-queue");
// Register implementations
worker.registerWorkflowImplementationTypes(OrderWorkflowImpl.class);
worker.registerActivitiesImplementations(
new PaymentActivityImpl(stripeClient),
new InventoryActivityImpl(warehouseClient),
new NotificationActivityImpl(emailService)
);
factory.start();
Workflow Layer
Workflows define the business logic — the what and when. They must be deterministic. This is the rule that trips up every newcomer. No System.currentTimeMillis(), no random numbers, no direct HTTP calls. If you need any of those, delegate to an activity.
Why? Because Temporal replays your workflow from the beginning whenever a worker restarts. If your workflow isn't deterministic, the replay produces different results and the whole thing falls apart.
@WorkflowInterface
public interface OrderWorkflow {
@WorkflowMethod
OrderResult processOrder(Order order);
@SignalMethod
void cancelOrder(String reason);
@QueryMethod
OrderStatus getStatus();
}
Activity Layer
Activities are where the side effects live. HTTP calls, database writes, file operations — everything non-deterministic goes here. Activities can fail and be retried independently of the workflow. This is what makes Temporal powerful: a flaky API call doesn't kill your entire workflow, it just retries that one activity.
@ActivityInterface
public interface PaymentActivity {
@ActivityMethod
PaymentResult chargeCustomer(String customerId, Money amount);
@ActivityMethod
void refundPayment(String paymentId);
}
The Execution Flow
This is the part I wish someone had drawn for me when I started with Temporal. The flow of a typical order processing workflow:
If the worker crashes after payment but before sending the email, Temporal replays the workflow, skips the already-completed activities (it remembers their results), and picks up exactly where it left off. No duplicate payments. No lost orders.
Patterns I Actually Use
The Saga Pattern
For workflows that need compensation (rollbacks), I treat each step as having a forward action and a compensating action:
If shipping fails, we refund the payment and release the inventory. Each compensation is itself an activity that can be retried.
Activity Retry Configuration
Don't use the defaults blindly. Tune your retries per activity based on what it calls:
private final PaymentActivity paymentActivity = Workflow.newActivityStub(
PaymentActivity.class,
ActivityOptions.newBuilder()
.setStartToCloseTimeout(Duration.ofSeconds(30))
.setRetryOptions(RetryOptions.newBuilder()
.setInitialInterval(Duration.ofSeconds(1))
.setMaximumInterval(Duration.ofSeconds(30))
.setBackoffCoefficient(2.0)
.setMaximumAttempts(3) // Payment: don't retry forever
.build())
.build()
);
A payment API should not retry indefinitely — you might charge the customer twice if the API is flaky but still processes requests. A notification email? Retry 10 times over an hour, who cares.
Common Mistakes
Non-deterministic workflow code. I've seen people call UUID.randomUUID() inside a workflow. It works the first time. Then Temporal replays the workflow after a worker restart and generates a different UUID, and suddenly your state is inconsistent. Use Workflow.randomUUID() instead.
Fat activities. An activity that makes 5 API calls and writes to 3 databases is not an activity — it's a workflow disguised as an activity. If any one of those 5 calls fails, the entire activity retries from scratch. Break it up.
Ignoring versioning. When you change a workflow that's currently running, you need Workflow.getVersion() to handle in-flight executions gracefully. Skip this and you'll get non-determinism errors in production that are extremely confusing to debug.
How to cite
Pokhrel, N. (2026). "Temporal Java". Native Agents. https://nativeagents.dev/agent-skills/temporal/temporal-java