Implement the Service
This step implements the Operation at the center of the Service: back requestApproval with the approval Workflow, then start the development server and a Worker that hosts the Service, the Workflow, and the Activities.
Write the approval Workflow
The approval is an ordinary Temporal Workflow, as chosen in step 3. Nothing in it is Nexus-specific, and a Client could start it directly. The message handlers added in step 7 make it interactive.
The Workflow:
- Runs an Activity that evaluates whether the request can be auto-decided. In this walkthrough it is a placeholder; real logic would apply policy, check limits, or call a risk service.
- Runs an Activity that tells a human the request is waiting. Also a placeholder.
- Blocks until a decision arrives.
- Returns
APPROVEDorDENIED.
The blocking step is why this is a Workflow. It may wait weeks, across Worker restarts and deployments, and costs nothing while idle.
core/src/main/java/io/temporal/samples/nexuswalkthrough/handler/ApprovalWorkflowImpl.java
public class ApprovalWorkflowImpl implements ApprovalWorkflow {
private static final Logger logger = Workflow.getLogger(ApprovalWorkflowImpl.class);
private final ApprovalActivities activities =
Workflow.newActivityStub(
ApprovalActivities.class,
ActivityOptions.newBuilder().setStartToCloseTimeout(Duration.ofSeconds(10)).build());
// Durable intermediate state. This is the other reason the approval is a Workflow: an Activity
// could not hold any of it.
private SubmitDecisionInput.Decision decision;
private int remindersSent;
private final List<String> notes = new ArrayList<>();
@Override
public RequestApprovalOutput runApproval(RequestApprovalInput input) {
// Placeholder. Real logic would apply policy, check limits, or call a risk service.
activities.evaluateAutoDecision(input.getItemId(), input.getAmount());
// Placeholder. Real logic would page an approver, open a ticket, or send email.
activities.notifyApproverOfPendingRequest(input.getItemId(), input.getRequester());
// Block until submitDecision supplies a decision. This wait is durable and unbounded - the
// Worker can restart and redeploy while it is pending.
Workflow.await(() -> decision != null);
logger.info(
"Approval for {} decided {} after {} reminder(s) and {} note(s)",
input.getItemId(),
decision.getValue(),
remindersSent,
notes.size());
// This return value becomes the result of the requestApproval Nexus Operation, delivered to
// every caller whose completion callback is attached to this Execution.
return new RequestApprovalOutput(Decisions.toRequestApprovalOutput(decision));
}
// STEP 7 - Signal handler. Records the nudge and returns nothing.
@Override
public void remindApprover() {
remindersSent++;
logger.info("Approver reminded, {} reminder(s) so far", remindersSent);
}
// STEP 8 - Signal handler reached through Signal-with-Start. When the note arrives before anyone
// has called requestApproval, the Signal-with-Start creates this Workflow and this handler runs
// on the fresh Execution.
@Override
public void attachContext(String note) {
notes.add(note);
logger.info("Context attached: {}", note);
}
// STEP 7 - The Update's validator. Runs before the handler and can reject the request without
// changing anything or writing to Event History. Throwing here rejects the Update; the Workflow
// is untouched and the caller's Operation fails.
@Override
public void validateSubmitDecision(SubmitDecisionInput.Decision decision) {
if (this.decision != null) {
throw new IllegalStateException(
"approval already decided " + this.decision.getValue() + ", cannot decide again");
}
}
// STEP 7 - Update handler. Records the decision, which satisfies the condition the Workflow
// method is blocked on, and returns confirmation to the caller. The validator above guarantees
// this runs at most once.
@Override
public SubmitDecisionOutput submitDecision(SubmitDecisionInput.Decision decision) {
this.decision = decision;
return new SubmitDecisionOutput(Decisions.toSubmitDecisionOutput(decision), remindersSent);
}
}
Implement the Operation with TemporalOperationHandler
Use TemporalOperationHandler for every Temporal-backed Operation. It is the entry point to the Temporal Operation Handler, and it lets an Operation later gain a Signal or change its backing without changing shape.
TemporalOperationHandler.create(...) gives your start handler a context, a Nexus-aware Client, and the Operation input. Call startWorkflow on that Client and return its result. The Operation completes when the Workflow returns, delivering the Workflow's return value to the caller.
Use the injected Client for anything that starts or messages an Execution. It propagates bidirectional links and request Ids, so the caller's Execution and the approval Workflow are connected in the UI with no extra code. A Client you create yourself works, but loses that linking.
core/src/main/java/io/temporal/samples/nexuswalkthrough/handler/ApprovalServiceImpl.java
@OperationImpl
public OperationHandler<RequestApprovalInput, RequestApprovalOutput> requestApproval() {
return TemporalOperationHandler.create(
(ctx, client, input) ->
client.startWorkflow(
ApprovalWorkflow.class,
ApprovalWorkflow::runApproval,
input,
WorkflowOptions.newBuilder()
.setWorkflowId(ApprovalWorkflowId.forItem(input.getItemId()))
.setWorkflowIdConflictPolicy(
WorkflowIdConflictPolicy.WORKFLOW_ID_CONFLICT_POLICY_USE_EXISTING)
.build()));
}
Set the Workflow Id from the item id, as decided in step 3.
By default, starting a Workflow whose Id is already running fails the Operation. The Operation has only started once its completion callback is attached to a Workflow, so failing loudly beats reporting success to a caller that would then wait forever. The code above already sets USE_EXISTING instead, because once another Operation can create the approval first, this one must attach to it. Step 8 explains why.
Start the development server
The Worker needs a running Temporal Service and a Namespace to poll. Start the development server with two dynamic config values that later steps rely on:
temporal server start-dev \
--dynamic-config-value 'nexusoperation.enableStandalone=true' \
--dynamic-config-value 'activity.enableCallbacks=true'
nexusoperation.enableStandalone lets a Client start an Operation without a caller Workflow,
which is how you run each Operation from the command line as you build it.
activity.enableCallbacks allows a completion callback on a Standalone Activity Execution, which
the notifyRequester Operation in step 9
needs.
In a second terminal, create the Namespace the handler runs in:
temporal operator namespace create --namespace approval-handler-namespace
Run the Worker
One Worker hosts the Nexus Service, Workflow, and Activity implementations. It polls approval-handler-task-queue, which the Nexus Endpoint targets in the next step.
core/src/main/java/io/temporal/samples/nexuswalkthrough/handler/HandlerWorker.java
public static void main(String[] args) {
WorkflowClient client = ClientOptions.getWorkflowClient(args);
WorkerFactory factory = WorkerFactory.newInstance(client);
Worker worker = factory.newWorker(DEFAULT_TASK_QUEUE_NAME);
worker.registerWorkflowImplementationTypes(ApprovalWorkflowImpl.class);
worker.registerActivitiesImplementations(new ApprovalActivitiesImpl());
worker.registerNexusServiceImplementation(new ApprovalServiceImpl());
factory.start();
}
The Worker that registers a Nexus Service doesn't have to run the backing Workflow. Larger deployments often split them; see Nexus patterns.
In that second terminal, start it from the repository root and leave it running:
./gradlew -q :core:execute \
-PmainClass=io.temporal.samples.nexuswalkthrough.handler.HandlerWorker \
--args="-target-host localhost:7233 -namespace approval-handler-namespace"
Qualify the task as :core:execute. An unqualified execute also runs the lambda-worker:starter
task, which ignores -PmainClass and starts an unrelated sample. The Worker runs until you stop it,
so Gradle keeps reporting the task as executing.
The Worker is polling, but no caller can reach the Service yet.
Step 5 publishes it and runs
requestApproval for the first time.
Handle failures
Callers can tell two kinds of failure apart:
- A contract violation, a payload the generated validator rejects, surfaces as
BAD_REQUEST. Retrying won't help, and the single error lists every violation. - An application failure, where the approval cannot proceed for a business reason, fails the Operation. Whether it retries depends on the error type you raise. See Nexus error handling.
- Temporal Operation Handler for the handler type and the Nexus-aware Client.
- Java Nexus feature guide for the full handler and Worker API.
- Nexus error handling for mapping failures to Nexus errors.
Next step
Step 5: Publish in Nexus
An Endpoint, and the first run of requestApproval.