Skip to content

Commit 2ca99b8

Browse files
authored
Sample code for Nexus walkthrough doc (#793)
* Sample code for Nexus walkthrough doc * Updated * Updating the sample code for the walkthrough * Adding Query * Updated NexGen * Addressing PR comments
1 parent 9d03432 commit 2ca99b8

31 files changed

Lines changed: 3719 additions & 0 deletions
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
# Nexus Microservice Development Walkthrough
2+
3+
Sample code for the [Nexus Microservice Development Walkthrough](https://docs.temporal.io/develop/java/nexus/development-walkthrough).
4+
5+
The walkthrough builds one Nexus Service from nothing to a complete API, adding a single Nexus
6+
capability at each step. This sample is the finished Service, with each piece kept separate so you
7+
can follow along step by step.
8+
9+
## The problem
10+
11+
A purchase request needs approval before it can proceed. Approval is slow and human-driven, so the
12+
system has to survive the wait. While a request is pending, other systems need to nudge the approver,
13+
check its progress, and attach information to it. Eventually a decision arrives, and the requesting
14+
system needs the outcome.
15+
16+
Each of those needs a different Nexus capability, which is what makes it a useful walkthrough.
17+
18+
## Operations, and what backs each one
19+
20+
| Operation | Backing | Walkthrough step |
21+
|---|---|---|
22+
| `requestApproval` | Workflow | Step 4 |
23+
| `remindApprover` | Signal, sent as sync messaging | Step 7 |
24+
| `getApprovalStatus` | Query, answered synchronously | Step 7 |
25+
| `submitDecision` | Workflow Update | Step 7 |
26+
| `attachApprovalContext` | Signal-with-Start | Step 8 |
27+
| `notifyRequester` | Standalone Activity | Step 9 |
28+
29+
## Sample directory structure
30+
31+
Read it in this order: the contract first, then what implements it, then what calls it.
32+
33+
### The contract
34+
35+
| File | What it does |
36+
|---|---|
37+
| [`approval.nexusrpc.yaml`](./approval.nexusrpc.yaml) | **The contract** (step 1). Six Operations and their types, in a language-neutral schema. The only thing the caller and handler share. |
38+
| [generatedservice/](./generatedservice) | **Every file here is generated** from the contract (step 2). See below. |
39+
40+
**Everything in [generatedservice](./generatedservice) is generated** - the Service interface, every
41+
input and output model, and the validation support classes. Each file carries a
42+
`// Generated by nexgen vX.Y.Z. DO NOT EDIT.` header and is overwritten on every generator run.
43+
Nothing outside that directory is generated.
44+
45+
| Generated file | What it is |
46+
|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---|
47+
| `ApprovalService.java` | The Service definition: one method per Operation. The handler implements it; the caller uses it as a Nexus stub. |
48+
| `*Input.java`, `*Output.java` | Typed models, one pair per Operation. Each carries its own Jackson serializer and deserializer. |
49+
| `Violation.java` | One contract violation. A bad payload reports every violation at once, in a single non-retryable `ApplicationFailure` of type `PayloadValidationError`, which a Nexus handler reports as `BAD_REQUEST`. |
50+
| `SpecNumbers.java` | Shared number parsing. Enforces JSON Schema numeric semantics and the +/-(2^53-1) integer cap, so a value Java accepts is also representable in TypeScript. |
51+
52+
The package is named `generatedservice` rather than `service` so that the import lines in the
53+
handler and the caller say plainly where those types come from. Nothing requires that naming, or
54+
even a separate package - generated types can live alongside the code that uses them. It is a
55+
convenience for reading this sample next to the walkthrough, not a rule to copy.
56+
57+
### The handler - the team publishing the Service
58+
59+
| File | What it does |
60+
|---|---|
61+
| [`handler/ApprovalServiceImpl.java`](./handler/ApprovalServiceImpl.java) | **The centerpiece.** All six Operations, in the order the walkthrough introduces them, each commented with the step it comes from and why it is backed the way it is. |
62+
| [`handler/ApprovalWorkflow.java`](./handler/ApprovalWorkflow.java) | The approval Workflow interface: the Workflow method, two Signal handlers, one Query handler, one Update handler (steps 4 and 7). |
63+
| [`handler/ApprovalWorkflowImpl.java`](./handler/ApprovalWorkflowImpl.java) | Runs two placeholder Activities, blocks until a decision arrives, returns it. The blocking is why this is a Workflow. |
64+
| [`handler/ApprovalWorkflowId.java`](./handler/ApprovalWorkflowId.java) | Derives the Workflow Id from the item id (step 3). One place, because two Operations have to agree on which Execution they mean. |
65+
| [`handler/ApprovalActivities.java`](./handler/ApprovalActivities.java) | Three Activities: two placeholders called from the Workflow, and `notifyRequester`, which backs an Operation directly as a Standalone Activity (step 9). |
66+
| [`handler/ApprovalActivitiesImpl.java`](./handler/ApprovalActivitiesImpl.java) | Placeholder implementations. They only log. |
67+
| [`handler/Decisions.java`](./handler/Decisions.java) | Converts a decision between the generated types that carry one. Exists only because the contract cannot declare a shared enum today. |
68+
| [`handler/HandlerWorker.java`](./handler/HandlerWorker.java) | The Worker hosting the Service, the Workflow, and the Activities (step 4). |
69+
70+
### The caller - a team in another Namespace
71+
72+
| File | What it does |
73+
|---|---|
74+
| [`caller/ApprovalCallerWorkflowImpl.java`](./caller/ApprovalCallerWorkflowImpl.java) | Calls all six Operations end to end (steps 6, 8, 10). Knows only the Endpoint name and the contract. |
75+
| [`caller/ApprovalCallerWorkflow.java`](./caller/ApprovalCallerWorkflow.java) | The caller Workflow interface. |
76+
| [`caller/CallerWorker.java`](./caller/CallerWorker.java) | The caller Worker. The one place the Endpoint name is bound to the Service. |
77+
| [`caller/CallerStarter.java`](./caller/CallerStarter.java) | Starts the caller Workflow once to run the full approval flow. |
78+
79+
### Supporting
80+
81+
| File | What it does |
82+
|---|---|
83+
| [`options/ClientOptions.java`](./options/ClientOptions.java) | Command line parsing for host, Namespace, TLS, and API key. A copy of the one in the other Nexus samples. |
84+
| [`description.md`](./description.md) | The Endpoint description, passed to `temporal operator nexus endpoint create`. |
85+
86+
### Regenerating the contract code
87+
88+
The generated code is committed, so you only need this after changing the contract. It was
89+
generated with [nexgen](https://github.com/temporalio/nexgen) v0.2.10; install the latest release
90+
from the [releases page](https://github.com/temporalio/nexgen/releases/latest).
91+
92+
```bash
93+
nexgen java \
94+
--output core/src/main/java/io/temporal/samples/nexuswalkthrough/generatedservice \
95+
--package-name io.temporal.samples.nexuswalkthrough.generatedservice \
96+
core/src/main/java/io/temporal/samples/nexuswalkthrough/approval.nexusrpc.yaml
97+
```
98+
99+
Two things to know. The Java generator requires the package name's last segment to match the output
100+
directory's name, which is why both end in `generatedservice`. And generation **never deletes
101+
files**: it overwrites the files it generates and leaves everything else in the output directory
102+
alone. When you rename or remove a type in the contract, delete its old generated file yourself.
103+
The contract sits outside `generatedservice/` so that the directory holds only generated code.
104+
105+
## Start the Temporal server
106+
107+
One dynamic config value is required. Everything else is default.
108+
109+
```bash
110+
temporal server start-dev --dynamic-config-value 'activity.enableCallbacks=true'
111+
```
112+
113+
`activity.enableCallbacks` allows attaching completion callbacks to standalone Activity Executions.
114+
The `notifyRequester` Operation is backed by a Standalone Activity, so without this setting it fails
115+
with `completion callbacks are not enabled for this namespace`. The other five Operations are
116+
unaffected.
117+
118+
No callback address allowlist is needed. The Nexus completion callback uses the `temporal://system`
119+
URL, which is always allowed; `callback.allowedAddresses` only matters for external endpoint
120+
targets.
121+
122+
## Create the namespaces and the Endpoint
123+
124+
In a separate terminal. The walkthrough crosses a real Namespace boundary, so the handler and the
125+
caller each get their own.
126+
127+
```bash
128+
temporal operator namespace create --namespace approval-handler-namespace
129+
temporal operator namespace create --namespace approval-caller-namespace
130+
131+
temporal operator nexus endpoint create \
132+
--name approval-endpoint \
133+
--target-namespace approval-handler-namespace \
134+
--target-task-queue approval-handler-task-queue \
135+
--description-file ./core/src/main/java/io/temporal/samples/nexuswalkthrough/description.md
136+
```
137+
138+
## Run it
139+
140+
Three terminals, from the repository root.
141+
142+
The task is `:core:execute`, qualified with the subproject. An unqualified `execute` also runs the
143+
`lambda-worker:starter` task, which ignores `-PmainClass` and starts an unrelated sample.
144+
145+
### Handler Worker
146+
147+
Hosts the Nexus Service, the approval Workflow, and the Activities. Like any Worker it runs until
148+
you stop it with Ctrl-C, so Gradle keeps reporting the task as executing.
149+
150+
```bash
151+
./gradlew -q :core:execute -PmainClass=io.temporal.samples.nexuswalkthrough.handler.HandlerWorker \
152+
--args="-target-host localhost:7233 -namespace approval-handler-namespace"
153+
```
154+
155+
### Caller Worker
156+
157+
```bash
158+
./gradlew -q :core:execute -PmainClass=io.temporal.samples.nexuswalkthrough.caller.CallerWorker \
159+
--args="-target-host localhost:7233 -namespace approval-caller-namespace"
160+
```
161+
162+
### Starter
163+
164+
```bash
165+
./gradlew -q :core:execute -PmainClass=io.temporal.samples.nexuswalkthrough.caller.CallerStarter \
166+
--args="-target-host localhost:7233 -namespace approval-caller-namespace"
167+
```
168+
169+
### Output
170+
171+
```
172+
INFO i.t.s.n.caller.CallerStarter - Purchase result: APPROVED
173+
```
174+
175+
The starter exits once the run finishes. The two Workers keep running until you stop them.
176+
177+
The handler Worker shows the run in order:
178+
179+
```
180+
INFO ApprovalWorkflowImpl - Context attached: Approved in the Q3 ergonomics budget
181+
INFO ApprovalActivitiesImpl - Evaluating auto-decision rules for standing-desk-... at 1250.0
182+
INFO ApprovalWorkflowImpl - Approver reminded, 1 reminder(s) so far
183+
INFO ApprovalActivitiesImpl - Approver notified that standing-desk-... from dana@example.com is waiting
184+
INFO ApprovalWorkflowImpl - Approval for standing-desk-... decided APPROVED after 1 reminder(s) and 1 note(s)
185+
INFO ApprovalActivitiesImpl - Notifying dana@example.com that their request was APPROVED
186+
```
187+
188+
Note the ordering: the context is attached **before** the approval is requested. That is
189+
Signal-with-Start creating the approval Workflow, which `requestApproval` then attaches to rather
190+
than failing on.
191+
192+
## What to look at afterwards
193+
194+
One approval Workflow, with its Workflow Id derived from the item id:
195+
196+
```bash
197+
temporal workflow list --namespace approval-handler-namespace
198+
```
199+
200+
The notification ran as a Standalone Activity, with no parent Workflow:
201+
202+
```bash
203+
temporal activity list --namespace approval-handler-namespace
204+
```
205+
206+
The caller's Event History shows each Operation, and which ones completed during the call versus
207+
through a completion callback:
208+
209+
```bash
210+
temporal workflow list --namespace approval-caller-namespace
211+
temporal workflow show --namespace approval-caller-namespace --workflow-id <caller-workflow-id>
212+
```
213+
214+
## Running against Temporal Cloud or a self-hosted service
215+
216+
Supply the relevant [CLI flags](./options/ClientOptions.java) to set up the connection. On Temporal
217+
Cloud you also have to add the caller Namespace to the Endpoint's allowed callers; creating the
218+
Endpoint is not enough, and the failure looks like a routing problem rather than a permissions one.
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
# WALKTHROUGH STEP 1 - Define the data contract.
2+
#
3+
# This file is the contract, and it is the only thing a caller and a handler share. It is written
4+
# before any implementation and in no particular language; step 2 generates the typed models,
5+
# validators, and Service definition for each language from it.
6+
#
7+
# Every Operation input and output is an object type rather than a bare value. An object can grow an
8+
# optional field later without breaking the wire format; a bare string cannot.
9+
#
10+
# This file lives outside generatedservice/ so that the directory holds only generated code. nexgen
11+
# overwrites the files it generates there but never deletes any, so remove a stale file yourself
12+
# when you rename or remove a type.
13+
# @@@SNIPSTART samples-java-nexus-walkthrough-contract
14+
nexusrpc: "1.0.0"
15+
$schema: https://json-schema.org/draft/2020-12/schema
16+
description: Purchase approval service built by the Nexus Microservice Development Walkthrough.
17+
18+
services:
19+
ApprovalService:
20+
fqn: temporal.samples.approval.v1.ApprovalService
21+
description: Start a purchase approval, message it while it is pending, and learn the outcome.
22+
operations:
23+
24+
# Backed by a Workflow. The Workflow's return value is this Operation's result - see step 4.
25+
requestApproval:
26+
description: Start an approval and return the decision once it is made.
27+
input: { $ref: "#/$defs/RequestApprovalInput" }
28+
output: { $ref: "#/$defs/RequestApprovalOutput" }
29+
30+
# A Signal. Fire-and-forget, so it declares no output - see step 7.
31+
remindApprover:
32+
description: Ask the approver again. Returns nothing.
33+
input: { $ref: "#/$defs/RemindApproverInput" }
34+
35+
# A Query. Reads in-flight progress without changing it. It does not return the decision;
36+
# requestApproval does - see step 7.
37+
getApprovalStatus:
38+
description: Report the progress of an approval without changing it.
39+
input: { $ref: "#/$defs/GetApprovalStatusInput" }
40+
output: { $ref: "#/$defs/GetApprovalStatusOutput" }
41+
42+
# An Update. The caller needs confirmation back, which is what makes this an Update rather
43+
# than a Signal - see step 7.
44+
submitDecision:
45+
description: Supply the decision and confirm it was recorded.
46+
input: { $ref: "#/$defs/SubmitDecisionInput" }
47+
output: { $ref: "#/$defs/SubmitDecisionOutput" }
48+
49+
# Signal-with-Start. Its input repeats the purchase details because it may have to create the
50+
# approval it is messaging - see step 8.
51+
attachApprovalContext:
52+
description: Attach supporting information to a purchase, whether or not its approval exists yet.
53+
input: { $ref: "#/$defs/AttachApprovalContextInput" }
54+
55+
# Backed by a Standalone Activity - one durable step, no Workflow - see step 9.
56+
notifyRequester:
57+
description: Notify the requester once the decision is final.
58+
input: { $ref: "#/$defs/NotifyRequesterInput" }
59+
output: { $ref: "#/$defs/NotifyRequesterOutput" }
60+
61+
# The APPROVED | DENIED value set is declared inline on each property that carries it, rather than
62+
# once under $defs. A named enum under $defs is rejected by the generator today, so each Operation
63+
# gets its own nested value class; handler/Decisions.java converts between them.
64+
$defs:
65+
66+
RequestApprovalInput:
67+
type: object
68+
additionalProperties: false
69+
properties:
70+
itemId: { type: string }
71+
requester: { type: string }
72+
amount: { type: number }
73+
required: [itemId, requester, amount]
74+
75+
RequestApprovalOutput:
76+
type: object
77+
additionalProperties: false
78+
properties:
79+
decision:
80+
description: The outcome of the approval.
81+
type: string
82+
enum: [APPROVED, DENIED]
83+
required: [decision]
84+
85+
RemindApproverInput:
86+
type: object
87+
additionalProperties: false
88+
properties:
89+
itemId: { type: string }
90+
required: [itemId]
91+
92+
GetApprovalStatusInput:
93+
type: object
94+
additionalProperties: false
95+
properties:
96+
itemId: { type: string }
97+
required: [itemId]
98+
99+
GetApprovalStatusOutput:
100+
type: object
101+
additionalProperties: false
102+
properties:
103+
pending: { description: True until a decision is recorded., type: boolean }
104+
remindersSent: { description: How many reminders have been sent so far., type: integer }
105+
notesAttached: { description: How many notes have been attached so far., type: integer }
106+
required: [pending, remindersSent, notesAttached]
107+
108+
SubmitDecisionInput:
109+
type: object
110+
additionalProperties: false
111+
properties:
112+
itemId: { type: string }
113+
decision:
114+
description: The decision being submitted.
115+
type: string
116+
enum: [APPROVED, DENIED]
117+
required: [itemId, decision]
118+
119+
SubmitDecisionOutput:
120+
type: object
121+
additionalProperties: false
122+
properties:
123+
recorded:
124+
description: The decision that was recorded.
125+
type: string
126+
enum: [APPROVED, DENIED]
127+
remindersSent: { description: How many reminders were sent before the decision., type: integer }
128+
required: [recorded, remindersSent]
129+
130+
AttachApprovalContextInput:
131+
type: object
132+
additionalProperties: false
133+
properties:
134+
itemId: { type: string }
135+
requester: { type: string }
136+
amount: { type: number }
137+
note: { description: The supporting information to attach., type: string }
138+
required: [itemId, requester, amount, note]
139+
140+
NotifyRequesterInput:
141+
type: object
142+
additionalProperties: false
143+
properties:
144+
requester: { type: string }
145+
decision:
146+
description: The final decision.
147+
type: string
148+
enum: [APPROVED, DENIED]
149+
required: [requester, decision]
150+
151+
NotifyRequesterOutput:
152+
type: object
153+
additionalProperties: false
154+
properties:
155+
deliveredTo: { description: Where the notification was sent., type: string }
156+
required: [deliveredTo]
157+
# @@@SNIPEND

0 commit comments

Comments
 (0)