Skip to main content

Decoupling Temporal Services with Nexus

Author: Nikolay Advolodkin | Editor: Angela Zhou

In this walkthrough, you'll take a monolithic Temporal application — where Payments and Compliance share a single Worker — and split it into two independently deployable services connected through Temporal Nexus.

You'll define a shared service contract, implement a synchronous Nexus handler, and rewire the caller — all while keeping the exact same business logic and workflow behavior. By the end, you'll understand how Nexus lets teams decouple without sacrificing durability.

What you'll learn

  • Register a Nexus Endpoint using the Temporal CLI
  • Define a shared Nexus Service contract between teams with @Service and @Operation
  • Implement a synchronous Nexus handler with @ServiceImpl and @OperationImpl
  • Swap a local Activity call for a durable cross-team Nexus call
  • Inspect Nexus operations in the Web UI Event History

Prerequisites

Before you begin this walkthrough, ensure you have:

Scenario

You work at a bank where every payment flows through three steps:

  1. Validate the payment (amount, accounts)
  2. Check compliance (risk assessment, sanctions screening)
  3. Execute the payment (call the gateway)

Two teams split this work:

TeamOwnsTask Queue
PaymentsSteps 1 & 3 — validate and executepayments-processing
ComplianceStep 2 — risk assessment & regulatory checkscompliance-risk

The Problem

Right now, both teams' code runs on the same Worker. One process. One deployment. One blast radius.

Use the interactive diagram below to why this matters:

That's a problem because the Compliance team deals with sensitive regulatory work — OFAC sanctions screening, anti-money laundering (AML) monitoring, risk decisions — that requires stricter access controls, separate audit trails, and its own release cycle. Payments has none of those constraints. But because both teams share a single process, they're forced into the same failure domain, the same security perimeter, and the same deploy pipeline.

In practice, that shared fate plays out like this: Compliance ships a bug at 3 AM. Their code crashes. But it's running on the Payments Worker — so Payments goes down too. Same blast radius. Same 3 AM page. Two teams, one shared fate.

The obvious fix is splitting them into microservices with REST calls. But that introduces a new problem: if Compliance is down when Payments calls it, the request is lost. No retries. No durability. You're writing your own retry loops, circuit breakers, and dead letter queues. You've traded one problem for three.

The Solution: Temporal Nexus

Nexus gives you team boundaries with durability. Each team gets its own Worker, its own deployment pipeline, its own security perimeter, its own blast radius — while Temporal manages the durable, type-safe calls between them.

The Payments workflow calls the Compliance team through a Nexus operation. If the Compliance Worker goes down mid-call, the payment workflow just...waits. When Compliance comes back, it picks up exactly where it left off. No retry logic. No data loss. No 3am page for the Payments team.

The best part? The code change is almost invisible:

// BEFORE (monolith — direct activity call):
ComplianceResult compliance = complianceActivity.checkCompliance(compReq);

// AFTER (Nexus — durable cross-team call):
ComplianceResult compliance = complianceService.checkCompliance(compReq);

Same method name. Same input. Same output. Completely different architecture.

Here's what happens when the Compliance Worker goes down mid-call — and why it doesn't matter:

Nexus Durability svg

Why Nexus over REST or a shared Activity?

You could split Payments and Compliance into microservices with REST calls. But then you'd write your own retry loops, circuit breakers, and dead letter queues. Here's how the options compare:

REST / HTTPDirect Temporal ActivityTemporal Nexus
Worker goes downRequest lost, manual retrySame crash domainWorkflow pauses, auto-resumes
Retry logicWrite it yourselfTemporal retries within teamBuilt-in across the boundary
Type safetyOpenAPI + code genJava interfaceShared Java interface
Human reviewCustom callback URLsCouple teams together@UpdateMethod, durable wait
Team independenceShared failure domainShared deploymentSeparate Workers, blast radii
Code changeFull rewriteOne-line stub swap

New to Nexus? Try the Nexus Quick Start for a faster path. Come back here for the full decoupling exercise.


Overview

Architecture Overview: Payments and Compliance teams separated by a Nexus security boundary, with animated data flowing through validate, compliance check, and execute steps

The Payments team owns validation and execution (left). The Compliance team owns risk assessment, isolated behind a Nexus boundary (right). Data flows left-to-right — and if the Compliance side goes down mid-check, the payment resumes when it comes back.

What You'll Build

You'll start with a monolith where everything — the payment workflow, payment activities, and compliance checks — runs on a single Worker. By the end, you'll have two independent Workers: one for Payments and one for Compliance, communicating through a Nexus boundary.

BEFORE (Monolith):                    AFTER (Nexus Decoupled):
┌─────────────────────────┐ ┌──────────────┐ ┌──────────────┐
│ Single Worker │ │ Payments │ │ Compliance │
│ ───────────── │ │ Worker │ │ Worker │
│ Workflow │ │ ────── │ │ ────── │
│ PaymentActivity │ → │ Workflow │◄──►│ NexusHandler│
│ ComplianceActivity │ │ PaymentAct │ │ Checker │
│ │ │ │ │ │
│ ONE blast radius │ │ Blast #1 │ │ Blast #2 │
└─────────────────────────┘ └──────────────┘ └──────────────┘
▲ Nexus ▲

Checkpoint 0: Run the Monolith

tip

Don't forget to clone this repo for the exercise!

Before changing anything, let's see the system working. You need 3 terminal windows and a running Temporal server. Navigate into the java/decouple-monolith/exercise directory.

Terminal 0 — Temporal Server (if not already running):

temporal server start-dev

Create namespaces (one-time setup):

temporal operator namespace create --namespace payments-namespace
temporal operator namespace create --namespace compliance-namespace

Terminal 1 — Start the monolith Worker:

cd exercise
mvn compile exec:java@payments-worker

You should see:

Payments Worker started on: payments-processing
Registered: PaymentProcessingWorkflow, PaymentActivity
ComplianceActivity (monolith — will decouple)

Terminal 2 — Run the starter:

cd exercise
mvn compile exec:java@starter

Change the namespace in Temporal UI. You'll find this on the top of the Web UI.

Three transactions with different risk levels: TXN-A approved (low risk), TXN-B approved (medium risk), TXN-C declined (high risk, OFAC-sanctioned)

Expected results:

TransactionAmountRouteRiskResult
TXN-A$250US → USLOWCOMPLETED
TXN-B$12,000US → UKMEDIUMCOMPLETED
TXN-C$75,000US → USHIGHDECLINED_COMPLIANCE

Checkpoint 0 passed if all 3 transactions complete with the expected results. The system works! Now let's decouple it.

Stop the Worker (Ctrl+C in Terminal 1) before continuing.

tip

Are you enjoying this tutorial? Feel free to leave feedback with the Feedback widget on the side and sign up here to get notified when we drop new educational content!


Nexus Building Blocks

Before diving into code, here's a quick map of the 4 Nexus concepts you'll encounter:

Service    →    Operation    →    Endpoint      →      Registry
(contract) (method) (routing rule) (directory)
  • Nexus Service — A named collection of operations — the contract between teams. In this tutorial, that's the ComplianceNexusService interface. Think of it like the Activity interface you already have, but shared across services instead of internal to one Worker.
  • Nexus Operation — A single callable method on a Service, marked with @Operation (e.g., checkCompliance). This is the Nexus equivalent of an Activity method — the actual work the other team exposes.
  • Nexus Endpoint — A named routing rule that connects a caller to the right Namespace and Task Queue, so the caller doesn't need to know where the handler lives. You create compliance-endpoint and point it at the compliance-risk task queue.
  • Nexus Registry — The directory in Temporal where all Endpoints are registered. You register the endpoint once; callers look it up by name.
Quick match — test yourself!

Can you match each Nexus concept to what it represents in our payments scenario?


The TODOs

Pre-provided: The ComplianceWorkflow interface and implementation are already complete in the exercise. They use Temporal patterns you've already seen — @WorkflowMethod, @UpdateMethod, and Workflow.await(). Your work starts at TODO 1 — the Nexus-specific parts.

#FileActionKey Concept
1shared/nexus/ComplianceNexusService.javaYour work@Service + @Operation on both operations
2compliance/temporal/ComplianceNexusServiceImpl.javaYour workfromWorkflowHandle (async) + OperationHandler.sync (sync)
3compliance/temporal/ComplianceWorkerApp.javaYour workRegister workflow + Activity + Nexus handler
4payments/temporal/PaymentProcessingWorkflowImpl.javaModifyReplace Activity stub → Nexus stub
5payments/temporal/PaymentsWorkerApp.javaModifyAdd NexusServiceOptions, remove ComplianceActivity

Teaching order: Service interface (1) → Both handlers (2) → Worker (3, CLI) → Caller (4-5) → Run the human review path.


What we're building

Class Interaction Flow

Class Interaction Flow

The Compliance Workflow (already in the exercise)

Files: compliance/temporal/workflow/ComplianceWorkflow.java and ComplianceWorkflowImpl.java

Open them and read the code - they show the human-in-the-loop pattern you'll wire up through Nexus later. Here's the interface:

@WorkflowInterface
public interface ComplianceWorkflow {

@WorkflowMethod
ComplianceResult run(ComplianceRequest request);

@UpdateMethod
ComplianceResult review(boolean approved, String explanation);

@UpdateValidatorMethod(updateName = "review")
void validateReview(boolean approved, String explanation);
}

Let's look at the three methods:

  • run() (@WorkflowMethod) - The main entry point. Calls ComplianceActivity to score the transaction's risk, then sleeps 10s (for the Checkpoint 3 durability demo). For LOW and HIGH risk, it returns immediately with an auto-approve or auto-deny. For MEDIUM risk, it calls Workflow.await() to durably pause until a human reviewer submits a decision.
  • review() (@UpdateMethod) - Receives the human reviewer's approve/deny decision while the workflow is paused. Stores the result and unblocks run(), which then returns the final ComplianceResult.
  • validateReview() (@UpdateValidatorMethod) - A guard that rejects review Updates that arrive at the wrong time (e.g., before the workflow is waiting for review, or after a decision has already been made).
note

Why a workflow, not just an Activity? Using a workflow unlocks @UpdateMethod for MEDIUM-risk transactions - the workflow can pause and wait durably for a human reviewer's decision. A plain Activity can't do that. In the future, simple cases might just use an Activity, but a workflow gives you durability and human escalation for free.

Your work starts below at TODO 1.


TODO 1: Create the Nexus Service Interface

File: shared/nexus/ComplianceNexusService.java

This is the shared contract between teams — like an OpenAPI spec, but durable. Both teams depend on this interface.

What to add for TODO 1:

  1. @Service annotation on the interface - this registers the interface as a Nexus Service so Temporal knows it's a cross-team contract, not just a regular Java interface.
  2. @Operation annotation on both methods - this marks each method as a callable Nexus Operation. Without it, the method is just a Java method signature that Temporal won't expose through the Nexus boundary.
danger

The Nexus runtime validates all methods in a @Service interface at Worker startup. Every method must have @Operation — even ones you won't call right away — or the Worker will fail with Missing @Operation annotation.

Pattern to follow:

@Service
public interface ComplianceNexusService {
@Operation
ComplianceResult checkCompliance(ComplianceRequest request);

@Operation
ComplianceResult submitReview(ReviewRequest request);
}
tip

Look in the solution directory of the exercise repository if you need a hint!


TODO 2: Implement the Nexus Handlers

File: compliance/temporal/ComplianceNexusServiceImpl.java

This class implements both Nexus operations. You'll use two different handler patterns — one for starting a long-running workflow, one for interacting with an already-running workflow.

danger

Just like the interface needs @Operation on every method, the handler class needs an @OperationImpl method for every operation — or the Worker will fail at startup with Missing handlers for service operations.

Add two new annotations:

  • @ServiceImpl(service = ComplianceNexusService.class) — on the class
  • @OperationImpl — on each handler method

2a: checkCompliance — async handler (fromWorkflowHandle)

This handler receives compliance check requests from Payments. Instead of running business logic directly, it uses WorkflowRunOperation.fromWorkflowHandle() to start a ComplianceWorkflow and return a handle that binds the Nexus operation to that workflow's ID. On retries (transient failures), Temporal matches on the handle and reuses the existing workflow instead of starting a duplicate.

Nexus handle retry diagram: first call starts a workflow and returns a handle, retries reuse the same workflow instead of creating duplicates

Step 1: Annotate the class with @ServiceImpl to link it to the service interface:

@ServiceImpl(service = ComplianceNexusService.class)
public class ComplianceNexusServiceImpl {

Step 2: Create an @OperationImpl method that returns an OperationHandler. Use WorkflowRunOperation.fromWorkflowHandle - this is the pattern for wiring a Nexus operation to a long-running workflow:

    @OperationImpl
public OperationHandler<ComplianceRequest, ComplianceResult> checkCompliance() {
return WorkflowRunOperation.fromWorkflowHandle((ctx, details, input) -> {

Step 3: Inside the lambda, get a WorkflowClient from the Nexus context, create a workflow stub, and return a WorkflowHandle that links this Nexus operation to the workflow:

            WorkflowClient client = Nexus.getOperationContext().getWorkflowClient();
ComplianceWorkflow wf = client.newWorkflowStub(
ComplianceWorkflow.class,
WorkflowOptions.newBuilder()
.setTaskQueue("compliance-risk")
.setWorkflowId("compliance-" + input.getTransactionId())
.build());

return WorkflowHandle.fromWorkflowMethod(wf::run, input);
});
}

2b: submitReview — sync handler (OperationHandler.sync)

This handler receives a review decision (approve/deny) and forwards it to the already-running ComplianceWorkflow as a Workflow Update.

Use OperationHandler.sync because the Update returns immediately — the workflow just stores the result. This completes well within the 10-second sync handler deadline.

    @OperationImpl
public OperationHandler<ReviewRequest, ComplianceResult> submitReview() {
return OperationHandler.sync((ctx, details, input) -> {
WorkflowClient client = Nexus.getOperationContext().getWorkflowClient();
ComplianceWorkflow wf = client.newWorkflowStub(
ComplianceWorkflow.class,
"compliance-" + input.getTransactionId());
return wf.review(input.isApproved(), input.getExplanation());
});
}

OperationHandler.sync must complete within 10 seconds. This is fine here because the review() Update returns immediately. Any slow operation (Activity, sleep, await) must go in a workflow started via fromWorkflowHandle.

Key differences between the two handlers:
checkCompliance (2a)submitReview (2b)
PatternfromWorkflowHandleOperationHandler.sync
What it doesStarts a new long-running workflowSends Update to an existing workflow
DurabilityAsync — workflow runs independentlySync — must complete in 10 seconds
Retry behaviorRetries reuse the same workflowUpdate is idempotent if workflow ID is stable
Full code:
@ServiceImpl(service = ComplianceNexusService.class)
public class ComplianceNexusServiceImpl {

@OperationImpl
public OperationHandler<ComplianceRequest, ComplianceResult> checkCompliance() {
return WorkflowRunOperation.fromWorkflowHandle((ctx, details, input) -> {
WorkflowClient client = Nexus.getOperationContext().getWorkflowClient();
ComplianceWorkflow wf = client.newWorkflowStub(
ComplianceWorkflow.class,
WorkflowOptions.newBuilder()
.setTaskQueue("compliance-risk")
.setWorkflowId("compliance-" + input.getTransactionId())
.build());

return WorkflowHandle.fromWorkflowMethod(wf::run, input);
});
}

@OperationImpl
public OperationHandler<ReviewRequest, ComplianceResult> submitReview() {
return OperationHandler.sync((ctx, details, input) -> {
WorkflowClient client = Nexus.getOperationContext().getWorkflowClient();
ComplianceWorkflow wf = client.newWorkflowStub(
ComplianceWorkflow.class,
"compliance-" + input.getTransactionId());
return wf.review(input.isApproved(), input.getExplanation());
});
}
}

Quick Check

Q1: What does @ServiceImpl(service = ComplianceNexusService.class) tell Temporal?
@ServiceImpl links the handler class to its Nexus service interface. Temporal uses this to route incoming Nexus operations to the correct handler.
Q2: Why does the handler start a workflow instead of calling ComplianceChecker.checkCompliance() directly?
Handlers should only use Temporal primitives (workflow starts, queries, updates). Business logic belongs in activities, which are invoked by workflows. This keeps the handler thin and the architecture consistent.

TODO 3: Create the Compliance Worker

File: compliance/temporal/ComplianceWorkerApp.java

Standard CRWL pattern, but now with three registrations. Open the file - you'll see the Connect, Factory, and Launch steps are already written. The registration lines are commented out.

C — Connect to Temporal
R — Create factory and Worker on "compliance-risk"
W — Wire:
1. worker.registerWorkflowImplementationTypes(ComplianceWorkflowImpl.class)
2. worker.registerActivitiesImplementations(new ComplianceActivityImpl(new ComplianceChecker()))
3. worker.registerNexusServiceImplementation(new ComplianceNexusServiceImpl())
L — Launch

Uncomment the three lines inside the "Wire" section:

// TODO: W — Register workflow, activity, and Nexus handler
worker.registerWorkflowImplementationTypes(ComplianceWorkflowImpl.class);
worker.registerActivitiesImplementations(new ComplianceActivityImpl(new ComplianceChecker()));
worker.registerNexusServiceImplementation(new ComplianceNexusServiceImpl());

The first two are patterns you already know. The third is new - registerNexusServiceImplementation registers your Nexus handler so the Worker can receive incoming Nexus calls. Same shape, different method name.

Task queue: compliance-risk — remember this value. You'll use it again in Checkpoint 1.5 when you create the Nexus endpoint. The endpoint routes incoming Nexus calls to a task queue; the Worker polls that same queue to pick them up. They must match.


Checkpoint 1: Compliance Worker Starts

cd exercise
mvn compile exec:java@compliance-worker

Checkpoint 1 passed if you see:

Compliance Worker started on: compliance-risk
danger

If it fails to compile or crashes at startup, check:

  • TODO 1: Does ComplianceNexusService have @Service and @Operation on both methods?
  • TODO 2: Does ComplianceNexusServiceImpl have @ServiceImpl and @OperationImpl on both handlers (checkCompliance and submitReview)?
  • TODO 3: Are you registering the Workflow, Activity, and Nexus service?

Keep the compliance Worker running — you'll need it for Checkpoint 2.


Checkpoint 1.5: Create the Nexus Endpoint

Now that the compliance side is built, register the Nexus endpoint with Temporal. This tells Temporal: "When someone calls compliance-endpoint, route it to the compliance-risk task queue in compliance-namespace."

temporal operator nexus endpoint create \
--name compliance-endpoint \
--target-namespace compliance-namespace \
--target-task-queue compliance-risk

You should see:

Endpoint compliance-endpoint created.

Analogy: This is like adding a contact to your phone. The endpoint name is the contact name; the task queue + namespace is the phone number. You only do this once.

Without this, the Payments Worker (TODO 5) won't know where to route ComplianceNexusService calls.


TODO 4: Replace Activity Stub with Nexus Stub

File: payments/temporal/PaymentProcessingWorkflowImpl.java

Key teaching moment: You're swapping one line of code that changes the entire architecture.

Where does the endpoint come from? Not here! The workflow only knows the service (ComplianceNexusService). The endpoint ("compliance-endpoint") is configured in the Worker (TODO 5). This keeps the workflow portable - you can point it at a different endpoint in staging vs production without changing workflow code.

BEFORE:

private final ComplianceActivity complianceActivity =
Workflow.newActivityStub(ComplianceActivity.class, ACTIVITY_OPTIONS);

// In processPayment():
ComplianceResult compliance = complianceActivity.checkCompliance(compReq);

AFTER:

private final ComplianceNexusService complianceService = Workflow.newNexusServiceStub(
ComplianceNexusService.class,
NexusServiceOptions.newBuilder()
.setOperationOptions(NexusOperationOptions.newBuilder()
.setScheduleToCloseTimeout(Duration.ofMinutes(10))
.build())
.build());

// In processPayment():
ComplianceResult compliance = complianceService.checkCompliance(compReq);

What changed: Drag each Nexus replacement to its monolith equivalent:


TODO 5: Update the Payments Worker

File: payments/temporal/PaymentsWorkerApp.java

Two changes:

CHANGE 1: Register both workflows with NexusServiceOptions (maps service to endpoint):

worker.registerWorkflowImplementationTypes(
WorkflowImplementationOptions.newBuilder()
.setNexusServiceOptions(Collections.singletonMap(
"ComplianceNexusService", // interface name (no package)
NexusServiceOptions.newBuilder()
.setEndpoint("compliance-endpoint") // matches CLI endpoint
.build()))
.build(),
PaymentProcessingWorkflowImpl.class,
ReviewCallerWorkflowImpl.class); // both workflows use the same Nexus endpoint

CHANGE 2: Remove ComplianceActivityImpl registration:

// DELETE these lines:
ComplianceChecker checker = new ComplianceChecker();
worker.registerActivitiesImplementations(new ComplianceActivityImpl(checker));

Analogy: You're removing the compliance department from your building and adding a phone extension to their new office. The workflow dials the same number (checkCompliance), but the call now routes across the street.


Checkpoint 2: Decoupled End-to-End (Automated Decisions)

You need 3 terminal windows now:

Terminal 0: Temporal server (already running)

Terminal 1 — Compliance Worker (already running from Checkpoint 1, or restart):

cd exercise
mvn compile exec:java@compliance-worker

Terminal 2 — Payments Worker (restart with your changes):

cd exercise
mvn compile exec:java@payments-worker

Terminal 3 — Starter:

cd exercise
mvn compile exec:java@starter

TXN-A and TXN-C each take about 10 seconds (the compliance workflow includes a durable sleep to demonstrate Checkpoint 3). TXN-B ($12K, US→UK) is MEDIUM risk - its ComplianceWorkflow scores the risk, then calls Workflow.await() to durably pause until a human reviewer submits a decision via the @UpdateMethod. Until that Update arrives, the workflow (and the Nexus operation bound to it) just waits. That's expected - you'll complete the human review path after Checkpoint 3.

Checkpoint 2 passed if you see these results:

TransactionRiskResultHow
TXN-ALOWCOMPLETEDAuto-approved (~10s)
TXN-BMEDIUMWaitingPaused for human review (you'll complete this after Checkpoint 3)
TXN-CHIGHDECLINED_COMPLIANCEAuto-denied (~10s, amount > $50K)

Two Workers, two blast radii, two independent teams. The automated compliance path works end-to-end through Nexus.

Check the Temporal UI at http://localhost:8233 — you should see Nexus operations in the workflow Event History!


Checkpoint 3: Durability Across the Boundary

This is where it gets fun. Let's prove that Nexus is durable.

Make sure both Workers are running and that any previous workflows have completed or been terminated.

Terminal 3 — Run the starter:

cd exercise
mvn compile exec:java@starter

The starter runs TXN-A first. TXN-A has a 10-second durable sleep in ComplianceWorkflowImpl. During that 10-second window:

Terminal 1 — Kill the compliance Worker (Ctrl+C)

Now watch what happens:

  1. Terminal 3 (starter) — hangs. It's waiting for the TXN-A result. No crash, no error.
  2. Temporal UI (http://localhost:8233) — open the payment-TXN-A workflow. You'll see the Nexus operation in a backing off state. Temporal knows the compliance Worker is gone and is waiting for it to come back.

Terminal 1 — Restart the compliance Worker:

cd exercise
mvn compile exec:java@compliance-worker

Now watch:

  1. Terminal 1 (compliance Worker) — picks up the work immediately. You'll see [ComplianceChecker] Evaluating TXN-A in the logs.
  2. Terminal 3 (starter) — TXN-A completes with COMPLETED. The starter moves on to TXN-B and TXN-C as if nothing happened.
  3. Temporal UI — the Nexus operation shows as completed. No retries of the payment workflow. No duplicate compliance checks. The system just resumed.

Checkpoint 3 passed if TXN-A completes successfully after you restart the compliance Worker.

What just happened: The payment workflow didn't crash. It didn't timeout. It didn't lose data. It didn't need retry logic. It just... waited. When the compliance Worker came back, Temporal automatically routed the pending Nexus operation to it. Durability extends across the team boundary — that's the whole point of Nexus.


Complete the Human Review Path

You already implemented both handlers in TODO 2 — checkCompliance (async, fromWorkflowHandle) and submitReview (sync, OperationHandler.sync). Now let's use submitReview to approve TXN-B's MEDIUM-risk transaction.

Checkpoint: Approve TXN-B via Nexus

Make sure both Workers are running and TXN-B is still waiting from Checkpoint 2. If you need to restart, run the starter again first.

Terminal 4 — Approve TXN-B via Nexus:

cd exercise
mvn compile exec:java@review-starter

This starts a ReviewCallerWorkflow in the payments namespace that calls the submitReview Nexus operation. The Compliance team's sync handler receives it, looks up the compliance-TXN-B workflow, and sends the review Update — all through the Nexus boundary, with no direct access to the compliance workflow internals.

Want to deny instead? Edit ReviewStarter.java, change true to false, and re-run.

You should see the review result returned in Terminal 4, and back in Terminal 3, TXN-B completes with COMPLETED.

Checkpoint passed if TXN-B completes with COMPLETED after running the review starter.


Quiz

Test your understanding before moving on:

Q1: Where is the Nexus endpoint name (compliance-endpoint) configured?
In PaymentsWorkerApp, via NexusServiceOptionssetEndpoint("compliance-endpoint"). The workflow only knows the service interface. The Worker knows the endpoint. This separation keeps the workflow portable.
Q2: What happens if the Compliance Worker is down when the Payments workflow calls checkCompliance()?
The Nexus operation will be retried by Temporal until the scheduleToCloseTimeout expires (10 minutes in our case). If the Compliance Worker comes back within that window, the operation completes successfully. The Payment workflow just waits — no crash, no data loss.
Q3: What's the difference between @Service/@Operation and @ServiceImpl/@OperationImpl?
  • @Service / @Operation go on the interface — the shared contract both teams depend on
  • @ServiceImpl / @OperationImpl go on the handler class — the implementation that only the Compliance team owns

Think of it as: the interface is the menu (shared), the handler is the kitchen (private).

Q4: What's wrong with using OperationHandler.sync() to back a Nexus operation with a long-running workflow?
sync() starts a workflow and blocks for its result in a single handler call. If the Nexus operation retries (which happens during timeouts or transient failures), the handler runs again from scratch — starting a duplicate workflow each time.

The fix is WorkflowRunOperation.fromWorkflowHandle(), which returns a handle (like a receipt number) binding the Nexus operation to that workflow's ID. On retries, the infrastructure sees the handle and reuses the existing workflow instead of creating a new one.

Bad (creates duplicates on retry):

OperationHandler.sync((ctx, details, input) -> {
WorkflowClient client = Nexus.getOperationContext().getWorkflowClient();
ComplianceWorkflow wf = client.newWorkflowStub(...);
WorkflowClient.start(wf::run, input);
return WorkflowStub.fromTyped(wf).getResult(ComplianceResult.class);
});

Good (retries reuse the same workflow):

WorkflowRunOperation.fromWorkflowHandle((ctx, details, input) -> {
WorkflowClient client = Nexus.getOperationContext().getWorkflowClient();
ComplianceWorkflow wf = client.newWorkflowStub(...);
return WorkflowHandle.fromWorkflowMethod(wf::run, input);
});
Q5: Why does the handler start a workflow instead of calling ComplianceChecker.checkCompliance() directly?

Sync handlers should only contain Temporal primitives — workflow starts and queries. Running arbitrary Java code (like ComplianceChecker.checkCompliance()) in a handler bypasses Temporal's durability guarantees.

The handler starts a ComplianceWorkflow and waits for its result. The actual business logic runs inside an Activity within the workflow, where it gets retries, timeouts, and heartbeats for free. Plus, the workflow can pause for human review via @UpdateMethod — something a direct call could never support.


What You Built

You started with a monolith and ended with two independent services connected through Nexus:

BEFORE (Monolith):                    AFTER (Nexus Decoupled):
┌─────────────────────────┐ ┌──────────────┐ ┌──────────────┐
│ Single Worker │ │ Payments │ │ Compliance │
│ ───────────── │ │ Worker │ │ Worker │
│ Workflow │ │ ────── │ │ ────── │
│ PaymentActivity │ → │ Workflow │◄──►│ NexusHandler│
│ ComplianceActivity │ │ PaymentAct │ │ Checker │
│ │ │ │ │ │
│ ONE blast radius │ │ Blast #1 │ │ Blast #2 │
└─────────────────────────┘ └──────────────┘ └──────────────┘
▲ Nexus ▲

Key concepts you used:

ConceptWhat you did
@Service + @OperationDefined the shared contract between teams
@ServiceImpl + @OperationImplImplemented the handler on the Compliance side
fromWorkflowHandleBacked a Nexus operation with a long-running workflow (retry-safe)
OperationHandler.syncSent a workflow Update through the Nexus boundary
Workflow.newNexusServiceStubReplaced the Activity stub with a Nexus stub (one-line swap)
NexusServiceOptionsMapped the service interface to the endpoint in the Worker
Nexus Endpoint (CLI)Registered the routing rule: endpoint name to namespace + task queue

The fundamental pattern: same method call, different architecture. The workflow still calls checkCompliance() - but the call now crosses a team boundary with full durability.

What's Next?

From here you can explore more advanced patterns - multi-step compliance pipelines, async human escalation chains, or cross-namespace Nexus operations. See the Nexus documentation to go deeper.

Don't forget to sign up here to get notified when we drop new educational content!

Feedback