|
| 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. |
0 commit comments