Skip to main content
Unlisted page
This page is unlisted. Search engines will not index it, and only users having a direct link can access it.

Step 3 - Choose the backing implementation

View Markdown

The contract says nothing about what runs behind an Operation. That is deliberate — it is the handler's private decision, and it can change later without touching callers.

There are three choices, and picking the wrong one is the most common source of trouble later. This step makes the choice for each Operation in the walkthrough's example approval Service, then implements the simplest one.

No backing Execution

The handler computes an answer and returns it. Nothing durable is created: no Workflow, no Activity, nothing to cancel, nothing in Event History.

This fits work that cannot meaningfully fail and returns immediately — applying a rule to the input, deriving a value, reading configuration the handler already holds. The Operation completes during the handler call, and the caller gets the answer in the response.

The limit is that you get no durability. The code runs inside the Nexus Operation handler, so it is bounded by the request deadline, and a failure fails the request rather than retrying a step. If the work can fail in a way you would want retried, it needs an Activity instead.

Workflow

More than one step, any need to wait, or any need for durable intermediate state.

The Operation starts a Workflow and completes when that Workflow returns, so the Workflow's return value is the Operation's result. Use this when the work orchestrates several Activities, needs a timer, needs to receive messages while it runs, or needs to survive a Worker restart partway through.

A Workflow that represents one long-lived thing and stays reachable for messages is still just a Workflow — what makes it interactive is that it has message handlers and a Workflow Id you can predict, not a different kind of primitive.

Standalone Activity

One step, no waiting, no state. Call an external API, run a computation, send a notification.

The Operation starts an Activity Execution with no parent Workflow, and completes when the Activity returns. You get retries, timeouts, and a durable record of every attempt without a wrapper Workflow that exists only to call one Activity.

The tradeoff is that an Activity cannot receive messages or hold state, and cancellation only works if the Activity heartbeats. See Nexus Standalone Activity.

The choice for the approval Service

OperationBackingWhy
checkApprovalRequiredNoneApplies a threshold to the input. Nothing to orchestrate, nothing that can fail in a retryable way
requestApprovalWorkflowBlocks for a human decision, holds the reminder count, and accepts messages while pending
remindApproverWorkflow (message)A Signal to the approval started by requestApproval
submitDecisionWorkflow (message)An Update to that same approval, because the caller needs a result back
attachApprovalContextWorkflow (message)A Signal that also starts the approval if it does not exist yet
notifyRequesterStandalone ActivityOne outbound notification, no state, nothing to wait for

Three of these are worth the contrast.

An approval has to be a Workflow. It exists for a while, it has identity, and other systems interact with it during its lifetime. Backing it with an Activity would not work at all — an Activity cannot block for a human and cannot receive a Signal.

The notification is the opposite. It is a single side effect with nothing to orchestrate, so a Workflow would add an Event History and a Workflow Id for no benefit. But it does touch the outside world and can fail, so it needs an Activity rather than nothing.

checkApprovalRequired is the case for no backing at all. Compare it against the notification: both are "one small thing," and they get opposite answers. Sending mail can fail and you want that retried with a record of each attempt. Comparing an amount to a threshold cannot fail in any way worth retrying, so an Activity Execution would be pure overhead.

Give the approval Workflow a stable Id

The approval needs a Workflow Id derived from the purchase, not a random one, so that later messages can find it. Deriving it from the item id means a caller that knows the item id can reach the right Execution without the handler handing out Workflow Ids.

This also makes the start idempotent. A retried Nexus start request targets the same Workflow Id rather than starting a second approval for one purchase.

Deriving it in one place keeps the two Operations that need it from drifting apart:

core/src/main/java/io/temporal/samples/nexuswalkthrough/handler/ApprovalWorkflowId.java

public static String forItem(String itemId) {
return "approval-" + itemId;
}

It matters again in step 8, where attachApprovalContext may start the approval before requestApproval is ever called. Both Operations derive the same Workflow Id from the same item id, which is what lets them agree on which Execution they mean.

Build the Operation that needs no backing

checkApprovalRequired needs no Workflow, no Activity, and no Worker registration beyond the Service itself, so it is the shortest path to a working Operation.

Implement it with TemporalOperationHandler like every other Operation, apply the threshold, and return a synchronous result. The Operation completes during the handler call.

core/src/main/java/io/temporal/samples/nexuswalkthrough/handler/ApprovalServiceImpl.java

@OperationImpl
public OperationHandler<CheckApprovalRequiredInput, CheckApprovalRequiredOutput>
checkApprovalRequired() {
return TemporalOperationHandler.create(
(ctx, client, input) ->
TemporalOperationResult.sync(
new CheckApprovalRequiredOutput(
input.getAmount() >= APPROVAL_THRESHOLD, APPROVAL_THRESHOLD)));
}

Why build it this way?

Nothing about this choice is permanent, and that is the point of making it with TemporalOperationHandler rather than a plain synchronous handler.

What backs an Operation is private to the handler, so it can change while the contract stays exactly where it is. If spend policy moves out of the handler and into a policy service, checkApprovalRequired becomes Activity-backed. If it grows into something with several steps — checking a budget, consulting delegation rules, waiting on a policy engine that is slow — it becomes Workflow-backed, the same shape as requestApproval. In every case the handler changes and no caller does, because the contract did not.

That is the reason to reach for TemporalOperationHandler even for an Operation this small. It is the single entry point for all three backings, so replacing a threshold comparison with a full Workflow later is an edit inside one method rather than a new Operation and a contract change.

For Reviewers

The next paragraph is about cost. I thought it was relevant as a big differentiator for opersations with no backing so I put it in - but I'm not sure I should?

In Temporal Cloud there is a cost argument too. Starting any Nexus Operation costs one Action in the caller's Namespace, whatever backs it — that much is fixed. What varies is the handler side: an Operation with no backing Execution adds nothing, while a Workflow or Activity backing bills Actions of its own, retries included. Handling the Nexus Operation itself is not billed. For work as small as a threshold comparison that is the difference between one Action and several, which is worth knowing when the Operation is called on every purchase. See Nexus pricing.

Nothing runs this Operation yet. Step 4 starts the Worker that hosts the Service, and step 5 makes it reachable, so this is the first Operation you will see respond once those are in place.

Next

Step 4 - Implement the Service - back the approval with a Workflow and run a Worker.

Back to the Microservice Development Walkthrough overview.

RESOURCES