DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Related

  • Orchestrating Zero-Downtime Deployments With Temporal
  • LangGraph Beginner to Advanced: Part 1 — Introduction to LangGraph and Some Basic Concepts
  • Design Automation in Closure Engineering: Building Parametric Assemblies With CATIA and VB Scripting
  • Recurrent Workflows With Cloud Native Dapr Jobs

Trending

  • The Documentation Crisis Nobody Sees: Why AI Agents Are Breaking Faster Than Humans Can Document Them
  • Stop Loading Everything into Redshift: A Spectrum + Iceberg Pattern for Hybrid Analytics
  • Engineering Closed-Loop Graph-RAG Systems, Part 1: From Retrieval to Reasoning
  • Parallel Kafka Batch Processing With Kotlin Coroutines in Spring Boot
  1. DZone
  2. Data Engineering
  3. Data
  4. A Practical Guide to Temporal Workflow Design Patterns

A Practical Guide to Temporal Workflow Design Patterns

Learn Temporal workflow design patterns for reliable distributed systems using durable execution, sagas, polling, fan-out/fan-in, signals, and versioning.

By 
Akhil Madineni user avatar
Akhil Madineni
·
Jun. 18, 26 · Analysis
Likes (0)
Comment
Save
Tweet
Share
161 Views

Join the DZone community and get the full member experience.

Join For Free

Long-running, distributed business processes often require careful coordination, state management, and fault handling. Temporal offers a code-first approach to durable workflows: developers write ordinary code for orchestration, and the Temporal service persists state, retries failed tasks, and resumes execution after failures. This shifts focus from plumbing (queues, retries, timeouts) to domain logic, but it also encourages reuse of proven patterns. 

The Temporal community and documentation highlight several orchestration patterns — for example, sagas, state machines/actors, polling strategies, fan-out/fan-in, and versioning patterns — that solve recurring problems in workflow design. This article surveys these patterns, explaining when and how to use them, with concise code snippets to illustrate their implementation in Temporal.

A classic pattern in distributed transactions is the Saga (compensating transaction). In a saga, a business process is broken into a sequence of steps, each with its own “undo” compensation. If any step fails, the saga executes compensations in reverse order to restore consistency. 

In Temporal, this maps naturally to a try/catch around activities or to the built-in Saga helper. For example, a vacation booking workflow might book a hotel, then a flight, then an excursion. Each step registers a compensation action before invoking the activity. If a failure occurs, the catch block calls saga.compensate() to run all registered compensations in reverse. The following Java-like snippet shows this approach:

Java
 
public void bookVacation(BookingInfo info) {
    Saga saga = new Saga(new Saga.Options.Builder().build());
    try {
        saga.addCompensation(activities::cancelHotel, info.getClientId());
        activities.bookHotel(info);
        saga.addCompensation(activities::cancelFlight, info.getClientId());
        activities.bookFlight(info);
        saga.addCompensation(activities::cancelExcursion, info.getClientId());
        activities.bookExcursion(info);
        // If all succeed, method returns normally.
    } catch (TemporalFailure e) {
        saga.compensate(); // undo previous steps
        throw e;           // propagate failure
    }
}


If any book* activity throws an exception, the catch invokes saga.compensate(), which calls cancelExcursion, cancelFlight, and cancelHotel in reverse order. This pattern ensures that even if the workflow crashes after partial work, Temporal’s durable execution will eventually resume the compensation sequence. Because Temporal workflows are persistent, the saga logic itself is recoverable – the service records each step and its compensation in the history. In effect, workflows become distributed state machines where try/catch embodies the saga pattern.

Polling and External Events

Workflows often need to wait for external processes or inputs. In Temporal, there are two main polling strategies. Frequent polling (short interval) is implemented inside an activity loop: the activity repeatedly attempts a call, sleeps briefly, and heartbeats after each iteration. 

Because long-running activities must heartbeat to show liveness, the loop invokes Activity.getExecutionContext().heartbeat(null) each cycle. For example, a polling activity might look like this:

Java
 
@Override
public String doPoll() {
    ActivityExecutionContext context = Activity.getExecutionContext();
    while (true) {
        try {
            return service.getServiceResult();
        } catch (TestServiceException e) {
            // Service not ready; will retry
        }
        // Heartbeat to prevent timeout, then sleep briefly
        context.heartbeat(null);
        sleep(POLL_DURATION_SECONDS);
    }
}


In this snippet, service.getServiceResult() is retried until it succeeds. Each loop iteration heartbeats and sleeps for a fixed interval. If the worker or service crashes, Temporal will resume the loop exactly where it left off. This pattern is ideal for rapid retries or waiting on resources that become available shortly.

For infrequent polling, Temporal relies on activity retry options instead of a custom loop. A workflow can call an activity once, but configure its retry backoff so that failures trigger re-execution after longer delays. In practice, one sets a high initial retry interval and backoff coefficient in the ActivityOptions at workflow time. The workflow code itself is just a single activity call (no loop needed). If the activity throws an error, Temporal automatically retries it later, waiting longer each time. This approach leverages the built-in retry policy (e.g., exponential backoff) for occasional checks.

To handle arbitrary external signals or time delays, Temporal workflows can also use Workflow.await(timeout, condition) or Workflow.newTimer(). For instance, a workflow might await a boolean flag that is set by a signal handler, or await a fixed timeout for human input. This avoids busy-wait loops at the workflow level. Signals themselves can come at any time; Temporal’s messaging system lets running workflows be interrupted by signals without polling. In short, Temporal workflows mix timers (Workflow.await) and external signals to wait efficiently. Frequent polling lives in an activity with heartbeats, whereas infrequent or one-off waits can use activity retry or workflow timers.

Parallel and Batch Processing

When processing large data sets or issuing many operations in parallel, Temporal’s fan-out/fan-in pattern is useful. A parent workflow can spawn multiple child workflows or activities concurrently and then wait for all to complete. This is commonly used for batch jobs, bulk queries, or any parallel computations.

The following example shows a “page-by-page” batch processing workflow. For each batch of records, it spawns a child workflow per record and then uses Promise.allOf() to wait for all children. When a batch is done, it can optionally continue-as-new to process the next page without growing history indefinitely:

Java
 
@Override
public int processBatch(int pageSize, int offset) {
    List<SingleRecord> records = recordLoader.getRecords(pageSize, offset);
    List<Promise<Void>> results = new ArrayList<>();
    for (SingleRecord record : records) {
        String childId = Workflow.getInfo().getWorkflowId() + "/" + record.getId();
        RecordProcessorWorkflow processor =
            Workflow.newChildWorkflowStub(RecordProcessorWorkflow.class,
                ChildWorkflowOptions.newBuilder().setWorkflowId(childId).build());
        results.add(Async.procedure(processor::processRecord, record));
    }
    // Wait for all child workflows to finish
    Promise.allOf(results).get();
    // If no more records, return result and finish
    if (records.isEmpty()) {
        return offset;
    }
    // Otherwise continue as new for the next batch (to reset history)
    return nextRun.processBatch(pageSize, offset + records.size());
}


In this code, each child workflow processes one record. The parent collects a list of Promise<Void> and calls Promise.allOf(...).get(), which blocks the parent until all child workflows complete. Using children allows highly parallel processing without overloading a single worker. After finishing a batch, the code checks if (records.isEmpty()) and returns; otherwise it calls a continueAsNew stub (nextRun) with an updated offset. This continueAsNew effectively starts a fresh workflow execution with a new history, avoiding unbounded history growth for long-running loops. As shown, Temporal’s Async and Promise primitives make parallel fan-out/fan-in straightforward.

Beyond paging, fan-out can apply to any use case needing parallel work (bulk updates, scatter-gather queries, etc.). Conversely, gathering results into a list or aggregation is just collecting activity/child results into a shared variable, which Temporal safely persists in the history.

Actor-Like Workflows and Event-Driven Patterns

Temporal workflows are naturally stateful and can run indefinitely, making them suitable for actor or process-manager patterns. A workflow can “sleep” or wait for signals, maintain in-memory state, and react to external events. Clients can use signals (@SignalMethod) to send events into a running workflow and queries (@QueryMethod) to read its state without affecting it. This allows workflows to act like autonomous entities.

For example, imagine a subscription service workflow. It starts with a customer on trial, waits for either trial expiration or a cancellation signal, then proceeds to billing periods. Signals like cancelSubscription() can interrupt the main flow. Meanwhile, queries like queryCustomerId() can retrieve the workflow’s state from outside. Temporal’s event system handles all this without polling: “a running workflow can receive external messages without polling, and clients can inspect workflow state at any time”. Internally, the workflow code can use Workflow.await(...) to pause until a signal sets a flag. Here’s a conceptual sketch (TypeScript/JavaScript style) of using signal and query definitions:

TypeScript
 
const abortSignal = defineSignal<[string]>('abort');
const updateSignal = defineSignal<[number]>('update');
const getStateQuery = defineQuery<State>('getState');

export async function statefulWorkflow(config: Config): Promise<Result> {
    let state: State = {...initial...};
    let aborted = false;
    setHandler(abortSignal, (reason: string) => { aborted = true; });
    setHandler(getStateQuery, () => state);
    // Main workflow logic:
    await condition(() => aborted, '1 minute');
    if (aborted) {
      // cleanup or compensation
      return { status: 'aborted' };
    }
    // ... continue normal processing
    return { status: 'completed' };
}


In this pattern, external callers would workflow.signal(abortSignal, reason) or workflow.query(getStateQuery). Temporal’s signal-and-query features implement a process manager-style pattern: a workflow can behave like an event-driven state machine, reacting to signals in real time and allowing external inspection. This is more robust than polling, and since all state changes happen in the workflow code, consistency is guaranteed. (If a query is issued while the workflow is mid-activity, it will reflect the last completed state.)

Note that newer Temporal releases also support Workflow Updates, which are like synchronous signals that can return values. In environments where Update is available, a workflow can reply to a message directly. Otherwise, a client can query state as a two-step “signal then query” process. Either way, this pattern empowers long-lived processes and human-in-the-loop steps.

Versioning and Evolving Workflows

Temporal requires workflow code to be deterministic, so changing logic in running workflows must be done carefully. The community and docs describe versioning strategies. For short-lived or rare workflows, one can deploy a new workflow definition (e.g. MyWorkflowV2) or use a new task queue for new versions. For long-lived workflows, Temporal’s Workflow.getVersion API lets the code branch on a version number recorded in the history. This is often called the “patch” strategy. For example:

Java
 
int version = Workflow.getVersion("checksumAdded", Workflow.DEFAULT_VERSION, 1);
if (version == Workflow.DEFAULT_VERSION) {
    activities.upload(targetBucket, targetFilename, data);
} else {
    long checksum = activities.calculateChecksum(data);
    activities.uploadWithChecksum(targetBucket, targetFilename, data, checksum);
}


Here, on first execution getVersion("checksumAdded", DEFAULT, 1) returns DEFAULT_VERSION and runs the original upload() call. When a new worker with updated code runs getVersion("checksumAdded", DEFAULT, 1) again, Temporal records version = 1 in the history. Future runs hit the else branch and use the new uploadWithChecksum() code. This ensures deterministic replay: workflows that started before the code change continue on the original branch, and newer executions use the new logic. After all old executions finish, the branching logic can often be removed.

Overall, versioning patterns let developers evolve workflows without breaking running executions. Temporal offers multiple options — definition names, task queues, and the getVersion API — each with trade-offs. (Using separate definitions or queues isolates versions at the cost of more infrastructure, while getVersion keeps a single codebase but requires planned version markers.) Regardless, versioning is a key pattern to safely deploy workflow updates in production.

Conclusion

Temporal’s durable workflow engine incorporates many built-in aids for complex process patterns. By applying established designs — such as sagas for compensating transactions, retry and heartbeat loops for polling, fan-out/fan-in via child workflows, and event-driven actors with signals/queries — engineers can build robust systems without manual boilerplate. 

Each pattern leverages Temporal features: workflows and activities, promises, signals, queries, and continuations. The examples above show how little code is needed: a few method calls and standard control structures achieve what would otherwise be elaborate orchestration logic. 

In practice, adopting these patterns means that failures are handled gracefully and state is managed cleanly. For example, the saga code snippet illustrates reversing partial work on error, while the parallel batch example shows how to process unbounded data safely with continueAsNew. In summary, understanding Temporal’s idioms — as documented by the Temporal team and community — empowers developers to focus on business logic while the platform ensures reliability. Mastery of these workflow patterns leads to systems that are easier to reason about, easier to maintain, and resilient in production.

Design workflow Data Types

Opinions expressed by DZone contributors are their own.

Related

  • Orchestrating Zero-Downtime Deployments With Temporal
  • LangGraph Beginner to Advanced: Part 1 — Introduction to LangGraph and Some Basic Concepts
  • Design Automation in Closure Engineering: Building Parametric Assemblies With CATIA and VB Scripting
  • Recurrent Workflows With Cloud Native Dapr Jobs

Partner Resources

×

Comments

The likes didn't load as expected. Please refresh the page and try again.

  • RSS
  • X
  • Facebook

ABOUT US

  • About DZone
  • Support and feedback
  • Community research

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 215
  • Nashville, TN 37211
  • [email protected]

Let's be friends:

  • RSS
  • X
  • Facebook