This repository contains the code that goes along with our Decoupling Temporal Services with Nexus tutorial. Please reference that tutorial to see how to use this repository.
See the Nexus documentation to explore more.
- Register a Nexus Endpoint using the Temporal CLI
- Define a shared Nexus Service contract between teams with
@Serviceand@Operation - Implement a synchronous Nexus handler with
@ServiceImpland@OperationImpl - Swap a local Activity call for a durable cross-team Nexus call
- Inspect Nexus operations in the Web UI Event History
| Directory | Directory Path |
|---|---|
| Exercise | java/decouple-monolith/exercise |
| Solution to Exercise | java/decouple-monlith/solution |
You can launch an exercise environment for this tutorial using GitHub Codespaces by following this walkthrough.
A hands-on lab version of this material, in Kotlin, lives under kotlin/. It runs as
an Instruqt track and is built for a live 90 minute workshop with a 40 minute lab block.
kotlin/
├── decouple-monolith/
│ ├── exercise/ Gradle KTS, TODOs 1 to 11 open
│ └── solution/ Gradle KTS, complete
├── sandbox/Dockerfile Temurin 21 + Temporal CLI + pre-warmed Gradle cache
└── instruqt/
├── track.yml no challenges: block, challenges auto-discovered
├── config.yml container `workshop`, 8192 MB, ports 8233/8080
├── track_scripts/ setup-workshop, cleanup-workshop
├── 01-run-the-monolith/
├── 02-the-shared-contract/
├── 03-the-compliance-side/
├── 04-the-payments-side/
└── 05-durability-and-human-review/
The Java tree under java/ is unchanged and is not used by the track.
Eclipse Temurin 21, the Temporal CLI, both Kotlin trees at /opt/workshop, and a
warmed Gradle cache. Both trees are compiled at build time, so a broken port fails the
image build rather than the workshop. Lab start stages /opt/workshop to
/root/workshop, starts the dev server, and creates payments-namespace and
compliance-namespace.
The Nexus Endpoint is deliberately not created at setup. Attendees create it themselves in challenge 3.
| # | Slug | Format | Files touched |
|---|---|---|---|
| 01 | run-the-monolith | observe | none |
| 02 | the-shared-contract | code | ComplianceNexusService.kt |
| 03 | the-compliance-side | code | ComplianceNexusServiceImpl.kt, ComplianceWorkerApp.kt, plus the Endpoint CLI |
| 04 | the-payments-side | code | PaymentProcessingWorkflowImpl.kt, PaymentsWorkerApp.kt |
| 05 | durability-and-human-review | observe | none |
Four of the five check-workshop scripts are exit 0. This is a live instructor-led
workshop, so the instructor is the feedback loop and progression is not gated on a
script. Two concrete reasons:
- Skip runs
solve-workshop, which copies solution files over the learner's work. Gating progression pushes a stuck learner toward destroying their own code to move on. - Check-click latency costs real minutes across a room of 20 in a 40 minute lab block.
Challenge 04 keeps a check because its failure mode is silent. A mistyped Endpoint name does not fail the Workflow, it hangs. That check is a diagnostic, not a gate: it fails only when it finds a Workflow stuck retrying its Workflow task, and passes otherwise. Both directions are tested. It passes on a healthy run with TXN-B parked for review, and fires with the endpoint-not-found message when the Endpoint is missing.
Solve scripts stay on every challenge. They power Skip and make instruqt track test an
end-to-end integration test.
If this track is ever reused self-paced, with no instructor watching, put the checks back. The git history has them.
Every challenge has six tabs in a fixed order. The tab-N buttons in assignment.md
are zero-indexed positions, not ids, so reordering tabs means remapping every button.
| Index | Tab | Type |
|---|---|---|
| tab-0 | Exercise | service, port 8080 (code-server) |
| tab-1 | Temporal UI | service, port 8233 |
| tab-2 | Terminal | terminal |
| tab-3 | Payments Worker | terminal |
| tab-4 | Compliance Worker | terminal |
| tab-5 | Solution | service, port 8080 (code-server) |
Exercise is first on purpose. Instruqt renders the first tab as the active one, so its iframe has real dimensions at load. code-server cannot lay itself out in a 0x0 iframe, which is what left the editor blank until a manual refresh. Solution is still position 5 and still boots hidden, so it may still need one refresh.
Both editor tabs share ONE code-server instance and select their directory with
?folder=. Instruqt loads every service tab's iframe at challenge start, including
hidden ones, and code-server cannot lay itself out in a 0x0 iframe. Running one
instance instead of two halves that exposure and matches the shape of
temporal-ai-agents-python-v4, the one Temporal track known to run code-server through
this proxy successfully.
# Validate before any push.
cd kotlin/instruqt && instruqt track validate
# First push only: register the slug server-side, then reconcile.
instruqt track create nexus-kotlin-decouple-monolith --title "Decouple a Monolith with Temporal Nexus (Kotlin)"
instruqt track push --force
instruqt track pull # populates track id and every tab id
git add kotlin/instruqt/ && git commit -m "Pin Instruqt track and tab ids"
# Simulate a learner end to end via the solve scripts.
instruqt track testBuild the sandbox image for linux/amd64. An arm64 image (the Apple Silicon default)
fails to start on Instruqt with an empty log:
cd kotlin
docker buildx build --platform linux/amd64 \
-f sandbox/Dockerfile -t ghcr.io/nadvolod/edu-nexus-kotlin-sandbox:latest --push .The image is pinned by digest, so any change under kotlin/decouple-monolith/
requires a rebuild AND a re-pin. Pushing the track alone will not ship it: the sandbox
keeps booting the old digest and attendees get stale exercise code. Only changes under
kotlin/instruqt/ are push-only. Re-pin with:
docker buildx imagetools inspect ghcr.io/nadvolod/edu-nexus-kotlin-sandbox:latest --format "{{.Manifest.Digest}}"CI pushes only the track on merge to main
(.github/workflows/build-and-push-kotlin.yml). It deliberately does not build the
image: the image lives in a personal GHCR namespace that this repo's GITHUB_TOKEN
cannot write to, so the build stays a manual step and the digest above is what ships.
That push is currently a no-op. This repo has no INSTRUQT_TOKEN secret, so the
push job warns and skips rather than failing the run. Until someone adds one (Instruqt
-> Team Settings -> API keys, then a repo secret named INSTRUQT_TOKEN), publishing is
manual: cd kotlin/instruqt && instruqt track push. Nothing else has to change when the
secret lands.
The caller grants packages: write even though it never builds an image. A caller's
permissions: block is the ceiling for every job in the called workflow, and GitHub
checks that ceiling when it loads the workflow, before any if: runs. A read-only
ceiling therefore fails the entire run at startup with zero jobs and no readable log,
even though build-image is disabled.
- A wrong Nexus Endpoint name hangs rather than fails. The server rejects the
command with
BadScheduleNexusOperationAttributes: endpoint "..." not foundat command-report time, outside Workflow code, so no try/catch can see it. The Workflow sitsRunningwith repeatedWORKFLOW_TASK_FAILED. Challenge 04's check detects the retry loop instead of waiting for a completion that never arrives. - Do not check
temporal task-queue describe --task-queue-type nexusfor a poller. The server keeps listing a poller for minutes after the Worker process exits, so any check built on it passes on a stale entry from a previous attempt. If you ever add a check that has to prove a Nexus handler is registered, boot a Worker and grep its own log for theNexus Pollerstartup line instead. Challenge 03's assignment teaches learners to read that same line by eye. PaymentGateway.executePaymentfails 10% of the time on purpose. Check scripts assert on final Workflow state, never on clean first-attempt output.- The review Update is time sensitive.
submitReviewis a sync Nexus Operation with a 10 second budget and the handler Workflow has to be running first. Challenge 05 polls forcompliance-TXN-Bbefore reviewing, and retries. - Kotlin diverges from
java/in two deliberate places.PaymentStarterstarts the three Workflows without blocking, so TXN-B parking for review cannot hold up TXN-C. The Workers print their startup banner withprintln, because the Java exercise callsWorkflow.getLoggerfrommain, which throws off a Workflow thread. track.ymland everyassignment.mdfrontmatter are rewritten on push. Comments are stripped and keys reordered. Keep durable explanation here, not in those files.config.ymlis left alone.