diff --git a/java/templates/java-gradle/.aiassistant/rules/java-api-and-pitfalls.md b/java/templates/java-gradle/.aiassistant/rules/java-api-and-pitfalls.md
index 3405ef1d..794c81c5 100644
--- a/java/templates/java-gradle/.aiassistant/rules/java-api-and-pitfalls.md
+++ b/java/templates/java-gradle/.aiassistant/rules/java-api-and-pitfalls.md
@@ -36,19 +36,16 @@ docker run -it docker.restate.dev/restatedev/restate-cli:latest invocations ls
**Gradle (build.gradle.kts):**
```kt
-// Annotation processor
-annotationProcessor("dev.restate:sdk-api-gen:2.4.1")
-
// For deploying as HTTP service
-implementation("dev.restate:sdk-java-http:2.4.1")
+implementation("dev.restate:sdk-java-http:2.9.0")
// Or for deploying using AWS Lambda
-implementation("dev.restate:sdk-java-lambda:2.4.1")
+implementation("dev.restate:sdk-java-lambda:2.9.0")
```
**Maven**:
```xml Java/Maven
- 2.4.1
+ 2.9.0
@@ -57,37 +54,20 @@ implementation("dev.restate:sdk-java-lambda:2.4.1")
sdk-java-http
${restate.version}
-
+
dev.restate
sdk-java-lambda
${restate.version}
-
-
-
- org.apache.maven.plugins
- maven-compiler-plugin
-
-
-
-
- dev.restate
- sdk-api-gen
- ${restate.version}
-
-
-
-
-
-
```
+On JDK 23+, pass `--enable-native-access=ALL-UNNAMED` as a JVM argument (or set `Enable-Native-Access: ALL-UNNAMED` in the JAR manifest) to silence the native-access warning printed at startup.
+
### Minimal Scaffold
```java
-import dev.restate.sdk.Context;
import dev.restate.sdk.annotation.Handler;
import dev.restate.sdk.annotation.Service;
import dev.restate.sdk.endpoint.Endpoint;
@@ -96,7 +76,7 @@ import dev.restate.sdk.http.vertx.RestateHttpServer;
@Service
public class MyService {
@Handler
- public String myHandler(Context ctx, String greeting) {
+ public String myHandler(String greeting) {
return greeting + "!";
}
@@ -120,9 +100,9 @@ curl localhost:8080/MyService/greet --json '"World"'
## Core Concepts
- Restate provides durable execution: if a handler crashes or the process restarts, Restate replays the handler from the last completed step, not from scratch.
-- All handlers receive a Context object (`ctx`) as their first parameter. Use ctx methods for all I/O and side effects.
+- Access all Restate functionality (state, calls, side effects, timers, ...) through the static methods on the `Restate` class (e.g. `Restate.state()`, `Restate.run(...)`, `Restate.sleep(...)`).
- Handlers take one optional JSON-serializable input parameter and return one JSON-serializable output.
-- Code generation produces typed client classes (e.g., `MyServiceClient`) from annotated service definitions.
+- Call other services through `Restate.service(...)` / `Restate.serviceHandle(...)` (and the virtual-object/workflow variants) with a method reference.
---
@@ -135,8 +115,7 @@ See minimal scaffold above.
### Virtual Object (Stateful, Keyed)
```java
-import dev.restate.sdk.ObjectContext;
-import dev.restate.sdk.SharedObjectContext;
+import dev.restate.sdk.Restate;
import dev.restate.sdk.annotation.Handler;
import dev.restate.sdk.annotation.Shared;
import dev.restate.sdk.annotation.VirtualObject;
@@ -147,14 +126,14 @@ import dev.restate.sdk.http.vertx.RestateHttpServer;
public class MyObject {
@Handler
- public String myHandler(ObjectContext ctx, String greeting) {
- String objectId = ctx.key();
+ public String myHandler(String greeting) {
+ String objectId = Restate.key();
return greeting + " " + objectId + "!";
}
@Shared
- public String myConcurrentHandler(SharedObjectContext ctx, String input) {
+ public String myConcurrentHandler(String input) {
return "my-output";
}
@@ -164,14 +143,12 @@ public class MyObject {
}
```
-- **Exclusive handlers** (`@Handler`): only one executes at a time per key. Use for writes. Receive `ObjectContext`.
-- **Shared handlers** (`@Shared`): run concurrently per key. Use for reads. Receive `SharedObjectContext`.
+- **Exclusive handlers** (`@Handler`): only one executes at a time per key. Use for writes. Have read/write state access via `Restate.state()`.
+- **Shared handlers** (`@Shared`): run concurrently per key. Use for reads. Have read-only state access.
### Workflow
```java
-import dev.restate.sdk.SharedWorkflowContext;
-import dev.restate.sdk.WorkflowContext;
import dev.restate.sdk.annotation.Shared;
import dev.restate.sdk.annotation.Workflow;
import dev.restate.sdk.endpoint.Endpoint;
@@ -181,7 +158,7 @@ import dev.restate.sdk.http.vertx.RestateHttpServer;
public class MyWorkflow {
@Workflow
- public String run(WorkflowContext ctx, String input) {
+ public String run(String input) {
// implement workflow logic here
@@ -189,7 +166,7 @@ public class MyWorkflow {
}
@Shared
- public String interactWithWorkflow(SharedWorkflowContext ctx, String input) {
+ public String interactWithWorkflow(String input) {
// implement interaction logic here
return "my result";
}
@@ -207,15 +184,16 @@ public class MyWorkflow {
## State Management
-Never use global variables for state -- it is not durable across restarts. Use `StateKey` with `ctx.get`/`ctx.set` instead (available on `ObjectContext` and `WorkflowContext`):
+Never use global variables for state -- it is not durable across restarts. Use `StateKey` with `Restate.state().get`/`Restate.state().set` instead:
```java
+var state = Restate.state();
StateKey STRING_STATE_KEY = StateKey.of("my-key", String.class);
-String stringState = ctx.get(STRING_STATE_KEY).orElse("my-default");
-ctx.set(STRING_STATE_KEY, "my-new-value");
-ctx.clear(STRING_STATE_KEY);
-ctx.clearAll();
-Collection keys = ctx.stateKeys();
+String stringState = state.get(STRING_STATE_KEY).orElse("my-default");
+state.set(STRING_STATE_KEY, "my-new-value");
+state.clear(STRING_STATE_KEY);
+state.clearAll();
+Collection keys = state.getAllKeys();
```
For generic types, use `TypeRef`:
@@ -232,26 +210,37 @@ private static final StateKey> ITEMS =
### Request-Response Calls
-Code generation creates typed client classes from annotated service definitions:
+Two client styles are available:
+
+- **Simple client** (`Restate.service(...)` / `Restate.virtualObject(...)` / `Restate.workflow(...)`): the call is awaited inline and returns the result directly. Use for straightforward request-response.
+- **Handle-based client** (`Restate.serviceHandle(...)` / `Restate.virtualObjectHandle(...)` / `Restate.workflowHandle(...)`, called with a method reference): returns a `DurableFuture` you await explicitly. Use it for invocation options (e.g. idempotency keys), timeouts, or concurrency.
```java
-String svcResponse = MyServiceClient.fromContext(ctx).myHandler(request).await();
-String objResponse = MyObjectClient.fromContext(ctx, objectKey).myHandler(request).await();
-String wfResponse = MyWorkflowClient.fromContext(ctx, workflowId).run(request).await();
+// Simple client: the call is awaited inline and returns the result directly.
+// Use this for straightforward request-response calls.
+String svcResponse = Restate.service(MyService.class).myHandler(request);
+String objResponse = Restate.virtualObject(MyObject.class, objectKey).myHandler(request);
+String wfResponse = Restate.workflow(MyWorkflow.class, workflowId).run(request);
+
+// Handle-based client: returns a DurableFuture that you await explicitly.
+// Use it for invocation options (e.g. an idempotency key), timeouts, or concurrency.
+// (virtualObjectHandle(...) / workflowHandle(...) work the same way.)
+String svcResult =
+ Restate.serviceHandle(MyService.class).call(MyService::myHandler, request).await();
```
### One-Way Calls (Fire-and-Forget)
```java
-MyServiceClient.fromContext(ctx).send().myHandler(request);
-MyObjectClient.fromContext(ctx, objectKey).send().myHandler(request);
-MyWorkflowClient.fromContext(ctx, workflowId).send().run(request);
+Restate.serviceHandle(MyService.class).send(MyService::myHandler, request);
+Restate.virtualObjectHandle(MyObject.class, objectKey).send(MyObject::myHandler, request);
+Restate.workflowHandle(MyWorkflow.class, workflowId).send(MyWorkflow::run, request);
```
### Delayed Calls
```java
-MyServiceClient.fromContext(ctx).send().myHandler(request, Duration.ofDays(5));
+Restate.serviceHandle(MyService.class).send(MyService::myHandler, request, Duration.ofDays(5));
```
### Generic Calls (String-Based Service/Method Names)
@@ -266,25 +255,26 @@ Target workflowTarget = Target.workflow("MyWorkflow", "wf-id", "run");
// Do the call
String response =
- ctx.call(Request.of(target, TypeTag.of(String.class), TypeTag.of(String.class), request))
+ Restate.call(
+ Request.of(target, TypeTag.of(String.class), TypeTag.of(String.class), request))
.await();
// Or send the message
-ctx.send(Request.of(target, TypeTag.of(String.class), TypeTag.of(String.class), request));
+Restate.send(Request.of(target, TypeTag.of(String.class), TypeTag.of(String.class), request));
```
---
-## Side Effects / ctx.run
+## Side Effects / Restate.run
-Never call external APIs, databases, or non-deterministic functions directly in a handler. Wrap them in `ctx.run`:
+Never call external APIs, databases, or non-deterministic functions directly in a handler. Wrap them in `Restate.run`:
```java
-// Wrap non-deterministic code in ctx.run
-String result = ctx.run("call external API", String.class, () -> callExternalAPI());
+// Wrap non-deterministic code in Restate.run
+String result = Restate.run("call external API", String.class, () -> callExternalAPI());
// Wrap with name for better tracing
-String namedResult = ctx.run("my-side-effect", String.class, () -> callExternalAPI());
+String namedResult = Restate.run("my-side-effect", String.class, () -> callExternalAPI());
```
- The first argument is a label used for observability and debugging.
@@ -296,21 +286,21 @@ String namedResult = ctx.run("my-side-effect", String.class, () -> callExternalA
## Deterministic Helpers
-Never use `Math.random()`, `System.currentTimeMillis()`, or `new Date()` directly -- they break deterministic replay. Use ctx helpers instead:
+Never use `Math.random()`, `System.currentTimeMillis()`, or `new Date()` directly -- they break deterministic replay. Use `Restate.random()` / `Restate.instantNow()` instead:
```java
-float value = ctx.random().nextFloat();
-UUID uuid = ctx.random().nextUUID();
+float value = Restate.random().nextFloat();
+UUID uuid = Restate.random().nextUUID();
```
---
## Durable Timers
-Never use `Thread.sleep`. Use `ctx.sleep` for durable delays that survive crashes and restarts:
+Never use `Thread.sleep`. Use `Restate.sleep` for durable delays that survive crashes and restarts:
```java
-ctx.sleep(Duration.ofHours(30));
+Restate.sleep(Duration.ofHours(30));
```
---
@@ -321,11 +311,11 @@ Awakeables pause execution until an external system signals completion:
```java
// Create awakeable
-Awakeable awakeable = ctx.awakeable(String.class);
+Awakeable awakeable = Restate.awakeable(String.class);
String awakeableId = awakeable.id();
// Send ID to external system
-ctx.run(() -> requestHumanReview(name, awakeableId));
+Restate.run("request-human-review", () -> requestHumanReview(name, awakeableId));
// Wait for result
String review = awakeable.await();
@@ -337,8 +327,8 @@ External systems can also resolve/reject via HTTP:
Or from another handler:
```java
-ctx.awakeableHandle(awakeableId).resolve(String.class, "Looks good!");
-ctx.awakeableHandle(awakeableId).reject("Cannot be reviewed");
+Restate.awakeableHandle(awakeableId).resolve(String.class, "Looks good!");
+Restate.awakeableHandle(awakeableId).reject("Cannot be reviewed");
```
---
@@ -350,10 +340,10 @@ Cross-handler signaling within a Workflow. No ID management needed.
```java
DurablePromiseKey REVIEW_PROMISE = DurablePromiseKey.of("review", String.class);
// Wait for promise
-String review = ctx.promise(REVIEW_PROMISE).future().await();
+String review = Restate.promise(REVIEW_PROMISE).future().await();
// Resolve promise from another handler
-ctx.promiseHandle(REVIEW_PROMISE).resolve(review);
+Restate.promiseHandle(REVIEW_PROMISE).resolve(review);
```
---
@@ -366,8 +356,10 @@ Use `DurableFuture` combinators, NOT `CompletableFuture`. Native combinators are
```java
// Wait for all to complete
-DurableFuture call1 = MyServiceClient.fromContext(ctx).myHandler("request1");
-DurableFuture call2 = MyServiceClient.fromContext(ctx).myHandler("request2");
+DurableFuture call1 =
+ Restate.serviceHandle(MyService.class).call(MyService::myHandler, "request1");
+DurableFuture call2 =
+ Restate.serviceHandle(MyService.class).call(MyService::myHandler, "request2");
DurableFuture.all(call1, call2).await();
```
@@ -388,9 +380,8 @@ String res = Select.select().or(call1).or(call2).await();
```java
var handle =
- MyServiceClient.fromContext(ctx)
- .send()
- .myHandler(request, req -> req.idempotencyKey("abc123"));
+ Restate.serviceHandle(MyService.class)
+ .send(MyService::myHandler, request, InvocationOptions.idempotencyKey("abc123"));
```
### Attach to a Running Invocation
@@ -415,7 +406,7 @@ All handler inputs/outputs and state values use Jackson JSON serialization by de
### Custom Serde
-Implement `Serde` for custom serialization when Jackson defaults are not sufficient (binary payloads, non-JSON formats, or types with custom encoding). Pass the serde when declaring a `StateKey`, `DurablePromiseKey`, awakeable, or `ctx.run` call.
+Implement `Serde` for custom serialization when Jackson defaults are not sufficient (binary payloads, non-JSON formats, or types with custom encoding). Pass the serde when declaring a `StateKey`, `DurablePromiseKey`, awakeable, or `Restate.run` call.
---
@@ -431,6 +422,21 @@ Note: the Java SDK uses `TerminalException`, NOT `TerminalError` (which is used
Any other exception type causes automatic retries with exponential backoff. For retry policy configuration, refer to the retry guide.
+### TerminalException metadata
+
+You can attach a string metadata map to a `TerminalException`. The metadata is propagated to callers and accessible via `getMetadata()`. Requires Restate Server >= 1.6.
+
+```java
+throw new TerminalException("Something went wrong", Map.of("correlationId", correlationId));
+```
+
+Callers can read the metadata:
+
+```java
+Map metadata = e.getMetadata();
+String correlationId = metadata.get("correlationId");
+```
+
---
## SDK Clients (External Invocations)
@@ -441,27 +447,28 @@ Use `Client` to call Restate handlers from outside a Restate context (e.g., from
Client restateClient = Client.connect("http://localhost:8080");
// Request-response
-String result = MyServiceClient.fromClient(restateClient).myHandler("Hi");
+String result = restateClient.service(MyService.class).myHandler("Hi");
// One-way
-MyServiceClient.fromClient(restateClient).send().myHandler("Hi");
+restateClient.serviceHandle(MyService.class).send(MyService::myHandler, "Hi");
// Delayed
-MyServiceClient.fromClient(restateClient).send().myHandler("Hi", Duration.ofSeconds(1));
+restateClient
+ .serviceHandle(MyService.class)
+ .send(MyService::myHandler, "Hi", Duration.ofSeconds(1));
// With idempotency key
-MyObjectClient.fromClient(restateClient, "Mary")
- .send()
- .myHandler("Hi", opt -> opt.idempotencyKey("abc"));
+restateClient
+ .virtualObjectHandle(MyObject.class, "Mary")
+ .send(MyObject::myHandler, "Hi", InvocationOptions.idempotencyKey("abc"));
```
---
## Java-Specific Pitfalls
-- **Code generation creates typed client classes** (e.g., `MyServiceClient`) from `@Service`/`@VirtualObject`/`@Workflow` annotations. Use these for type-safe calls.
- **Use Restate's future combinators, NOT `CompletableFuture`.** Native Java futures break deterministic replay.
-- **Never use `Thread.sleep`, `Math.random()`, or `System.currentTimeMillis()`** -- use Restate context actions instead.
+- **Never use `Thread.sleep`, `Math.random()`, or `System.currentTimeMillis()`** -- use `Restate` SDK actions instead.
- **Never use global mutable variables for state** -- use Restate's K/V store for durable state.
- **For detailed API reference:** use the MCP server or JavaDocs.
@@ -490,7 +497,7 @@ class MyServiceTestMethod {
@Test
void testMyHandler(@RestateClient Client ingressClient) {
// Create the service client from the injected ingress client
- var client = MyServiceClient.fromClient(ingressClient);
+ var client = ingressClient.service(MyService.class);
// Send request to service and assert the response
var response = client.myHandler("Hi");
diff --git a/java/templates/java-maven-quarkus/.aiassistant/rules/java-api-and-pitfalls.md b/java/templates/java-maven-quarkus/.aiassistant/rules/java-api-and-pitfalls.md
index 3405ef1d..794c81c5 100644
--- a/java/templates/java-maven-quarkus/.aiassistant/rules/java-api-and-pitfalls.md
+++ b/java/templates/java-maven-quarkus/.aiassistant/rules/java-api-and-pitfalls.md
@@ -36,19 +36,16 @@ docker run -it docker.restate.dev/restatedev/restate-cli:latest invocations ls
**Gradle (build.gradle.kts):**
```kt
-// Annotation processor
-annotationProcessor("dev.restate:sdk-api-gen:2.4.1")
-
// For deploying as HTTP service
-implementation("dev.restate:sdk-java-http:2.4.1")
+implementation("dev.restate:sdk-java-http:2.9.0")
// Or for deploying using AWS Lambda
-implementation("dev.restate:sdk-java-lambda:2.4.1")
+implementation("dev.restate:sdk-java-lambda:2.9.0")
```
**Maven**:
```xml Java/Maven
- 2.4.1
+ 2.9.0
@@ -57,37 +54,20 @@ implementation("dev.restate:sdk-java-lambda:2.4.1")
sdk-java-http
${restate.version}
-
+
dev.restate
sdk-java-lambda
${restate.version}
-
-
-
- org.apache.maven.plugins
- maven-compiler-plugin
-
-
-
-
- dev.restate
- sdk-api-gen
- ${restate.version}
-
-
-
-
-
-
```
+On JDK 23+, pass `--enable-native-access=ALL-UNNAMED` as a JVM argument (or set `Enable-Native-Access: ALL-UNNAMED` in the JAR manifest) to silence the native-access warning printed at startup.
+
### Minimal Scaffold
```java
-import dev.restate.sdk.Context;
import dev.restate.sdk.annotation.Handler;
import dev.restate.sdk.annotation.Service;
import dev.restate.sdk.endpoint.Endpoint;
@@ -96,7 +76,7 @@ import dev.restate.sdk.http.vertx.RestateHttpServer;
@Service
public class MyService {
@Handler
- public String myHandler(Context ctx, String greeting) {
+ public String myHandler(String greeting) {
return greeting + "!";
}
@@ -120,9 +100,9 @@ curl localhost:8080/MyService/greet --json '"World"'
## Core Concepts
- Restate provides durable execution: if a handler crashes or the process restarts, Restate replays the handler from the last completed step, not from scratch.
-- All handlers receive a Context object (`ctx`) as their first parameter. Use ctx methods for all I/O and side effects.
+- Access all Restate functionality (state, calls, side effects, timers, ...) through the static methods on the `Restate` class (e.g. `Restate.state()`, `Restate.run(...)`, `Restate.sleep(...)`).
- Handlers take one optional JSON-serializable input parameter and return one JSON-serializable output.
-- Code generation produces typed client classes (e.g., `MyServiceClient`) from annotated service definitions.
+- Call other services through `Restate.service(...)` / `Restate.serviceHandle(...)` (and the virtual-object/workflow variants) with a method reference.
---
@@ -135,8 +115,7 @@ See minimal scaffold above.
### Virtual Object (Stateful, Keyed)
```java
-import dev.restate.sdk.ObjectContext;
-import dev.restate.sdk.SharedObjectContext;
+import dev.restate.sdk.Restate;
import dev.restate.sdk.annotation.Handler;
import dev.restate.sdk.annotation.Shared;
import dev.restate.sdk.annotation.VirtualObject;
@@ -147,14 +126,14 @@ import dev.restate.sdk.http.vertx.RestateHttpServer;
public class MyObject {
@Handler
- public String myHandler(ObjectContext ctx, String greeting) {
- String objectId = ctx.key();
+ public String myHandler(String greeting) {
+ String objectId = Restate.key();
return greeting + " " + objectId + "!";
}
@Shared
- public String myConcurrentHandler(SharedObjectContext ctx, String input) {
+ public String myConcurrentHandler(String input) {
return "my-output";
}
@@ -164,14 +143,12 @@ public class MyObject {
}
```
-- **Exclusive handlers** (`@Handler`): only one executes at a time per key. Use for writes. Receive `ObjectContext`.
-- **Shared handlers** (`@Shared`): run concurrently per key. Use for reads. Receive `SharedObjectContext`.
+- **Exclusive handlers** (`@Handler`): only one executes at a time per key. Use for writes. Have read/write state access via `Restate.state()`.
+- **Shared handlers** (`@Shared`): run concurrently per key. Use for reads. Have read-only state access.
### Workflow
```java
-import dev.restate.sdk.SharedWorkflowContext;
-import dev.restate.sdk.WorkflowContext;
import dev.restate.sdk.annotation.Shared;
import dev.restate.sdk.annotation.Workflow;
import dev.restate.sdk.endpoint.Endpoint;
@@ -181,7 +158,7 @@ import dev.restate.sdk.http.vertx.RestateHttpServer;
public class MyWorkflow {
@Workflow
- public String run(WorkflowContext ctx, String input) {
+ public String run(String input) {
// implement workflow logic here
@@ -189,7 +166,7 @@ public class MyWorkflow {
}
@Shared
- public String interactWithWorkflow(SharedWorkflowContext ctx, String input) {
+ public String interactWithWorkflow(String input) {
// implement interaction logic here
return "my result";
}
@@ -207,15 +184,16 @@ public class MyWorkflow {
## State Management
-Never use global variables for state -- it is not durable across restarts. Use `StateKey` with `ctx.get`/`ctx.set` instead (available on `ObjectContext` and `WorkflowContext`):
+Never use global variables for state -- it is not durable across restarts. Use `StateKey` with `Restate.state().get`/`Restate.state().set` instead:
```java
+var state = Restate.state();
StateKey STRING_STATE_KEY = StateKey.of("my-key", String.class);
-String stringState = ctx.get(STRING_STATE_KEY).orElse("my-default");
-ctx.set(STRING_STATE_KEY, "my-new-value");
-ctx.clear(STRING_STATE_KEY);
-ctx.clearAll();
-Collection keys = ctx.stateKeys();
+String stringState = state.get(STRING_STATE_KEY).orElse("my-default");
+state.set(STRING_STATE_KEY, "my-new-value");
+state.clear(STRING_STATE_KEY);
+state.clearAll();
+Collection keys = state.getAllKeys();
```
For generic types, use `TypeRef`:
@@ -232,26 +210,37 @@ private static final StateKey> ITEMS =
### Request-Response Calls
-Code generation creates typed client classes from annotated service definitions:
+Two client styles are available:
+
+- **Simple client** (`Restate.service(...)` / `Restate.virtualObject(...)` / `Restate.workflow(...)`): the call is awaited inline and returns the result directly. Use for straightforward request-response.
+- **Handle-based client** (`Restate.serviceHandle(...)` / `Restate.virtualObjectHandle(...)` / `Restate.workflowHandle(...)`, called with a method reference): returns a `DurableFuture` you await explicitly. Use it for invocation options (e.g. idempotency keys), timeouts, or concurrency.
```java
-String svcResponse = MyServiceClient.fromContext(ctx).myHandler(request).await();
-String objResponse = MyObjectClient.fromContext(ctx, objectKey).myHandler(request).await();
-String wfResponse = MyWorkflowClient.fromContext(ctx, workflowId).run(request).await();
+// Simple client: the call is awaited inline and returns the result directly.
+// Use this for straightforward request-response calls.
+String svcResponse = Restate.service(MyService.class).myHandler(request);
+String objResponse = Restate.virtualObject(MyObject.class, objectKey).myHandler(request);
+String wfResponse = Restate.workflow(MyWorkflow.class, workflowId).run(request);
+
+// Handle-based client: returns a DurableFuture that you await explicitly.
+// Use it for invocation options (e.g. an idempotency key), timeouts, or concurrency.
+// (virtualObjectHandle(...) / workflowHandle(...) work the same way.)
+String svcResult =
+ Restate.serviceHandle(MyService.class).call(MyService::myHandler, request).await();
```
### One-Way Calls (Fire-and-Forget)
```java
-MyServiceClient.fromContext(ctx).send().myHandler(request);
-MyObjectClient.fromContext(ctx, objectKey).send().myHandler(request);
-MyWorkflowClient.fromContext(ctx, workflowId).send().run(request);
+Restate.serviceHandle(MyService.class).send(MyService::myHandler, request);
+Restate.virtualObjectHandle(MyObject.class, objectKey).send(MyObject::myHandler, request);
+Restate.workflowHandle(MyWorkflow.class, workflowId).send(MyWorkflow::run, request);
```
### Delayed Calls
```java
-MyServiceClient.fromContext(ctx).send().myHandler(request, Duration.ofDays(5));
+Restate.serviceHandle(MyService.class).send(MyService::myHandler, request, Duration.ofDays(5));
```
### Generic Calls (String-Based Service/Method Names)
@@ -266,25 +255,26 @@ Target workflowTarget = Target.workflow("MyWorkflow", "wf-id", "run");
// Do the call
String response =
- ctx.call(Request.of(target, TypeTag.of(String.class), TypeTag.of(String.class), request))
+ Restate.call(
+ Request.of(target, TypeTag.of(String.class), TypeTag.of(String.class), request))
.await();
// Or send the message
-ctx.send(Request.of(target, TypeTag.of(String.class), TypeTag.of(String.class), request));
+Restate.send(Request.of(target, TypeTag.of(String.class), TypeTag.of(String.class), request));
```
---
-## Side Effects / ctx.run
+## Side Effects / Restate.run
-Never call external APIs, databases, or non-deterministic functions directly in a handler. Wrap them in `ctx.run`:
+Never call external APIs, databases, or non-deterministic functions directly in a handler. Wrap them in `Restate.run`:
```java
-// Wrap non-deterministic code in ctx.run
-String result = ctx.run("call external API", String.class, () -> callExternalAPI());
+// Wrap non-deterministic code in Restate.run
+String result = Restate.run("call external API", String.class, () -> callExternalAPI());
// Wrap with name for better tracing
-String namedResult = ctx.run("my-side-effect", String.class, () -> callExternalAPI());
+String namedResult = Restate.run("my-side-effect", String.class, () -> callExternalAPI());
```
- The first argument is a label used for observability and debugging.
@@ -296,21 +286,21 @@ String namedResult = ctx.run("my-side-effect", String.class, () -> callExternalA
## Deterministic Helpers
-Never use `Math.random()`, `System.currentTimeMillis()`, or `new Date()` directly -- they break deterministic replay. Use ctx helpers instead:
+Never use `Math.random()`, `System.currentTimeMillis()`, or `new Date()` directly -- they break deterministic replay. Use `Restate.random()` / `Restate.instantNow()` instead:
```java
-float value = ctx.random().nextFloat();
-UUID uuid = ctx.random().nextUUID();
+float value = Restate.random().nextFloat();
+UUID uuid = Restate.random().nextUUID();
```
---
## Durable Timers
-Never use `Thread.sleep`. Use `ctx.sleep` for durable delays that survive crashes and restarts:
+Never use `Thread.sleep`. Use `Restate.sleep` for durable delays that survive crashes and restarts:
```java
-ctx.sleep(Duration.ofHours(30));
+Restate.sleep(Duration.ofHours(30));
```
---
@@ -321,11 +311,11 @@ Awakeables pause execution until an external system signals completion:
```java
// Create awakeable
-Awakeable awakeable = ctx.awakeable(String.class);
+Awakeable awakeable = Restate.awakeable(String.class);
String awakeableId = awakeable.id();
// Send ID to external system
-ctx.run(() -> requestHumanReview(name, awakeableId));
+Restate.run("request-human-review", () -> requestHumanReview(name, awakeableId));
// Wait for result
String review = awakeable.await();
@@ -337,8 +327,8 @@ External systems can also resolve/reject via HTTP:
Or from another handler:
```java
-ctx.awakeableHandle(awakeableId).resolve(String.class, "Looks good!");
-ctx.awakeableHandle(awakeableId).reject("Cannot be reviewed");
+Restate.awakeableHandle(awakeableId).resolve(String.class, "Looks good!");
+Restate.awakeableHandle(awakeableId).reject("Cannot be reviewed");
```
---
@@ -350,10 +340,10 @@ Cross-handler signaling within a Workflow. No ID management needed.
```java
DurablePromiseKey REVIEW_PROMISE = DurablePromiseKey.of("review", String.class);
// Wait for promise
-String review = ctx.promise(REVIEW_PROMISE).future().await();
+String review = Restate.promise(REVIEW_PROMISE).future().await();
// Resolve promise from another handler
-ctx.promiseHandle(REVIEW_PROMISE).resolve(review);
+Restate.promiseHandle(REVIEW_PROMISE).resolve(review);
```
---
@@ -366,8 +356,10 @@ Use `DurableFuture` combinators, NOT `CompletableFuture`. Native combinators are
```java
// Wait for all to complete
-DurableFuture call1 = MyServiceClient.fromContext(ctx).myHandler("request1");
-DurableFuture call2 = MyServiceClient.fromContext(ctx).myHandler("request2");
+DurableFuture call1 =
+ Restate.serviceHandle(MyService.class).call(MyService::myHandler, "request1");
+DurableFuture call2 =
+ Restate.serviceHandle(MyService.class).call(MyService::myHandler, "request2");
DurableFuture.all(call1, call2).await();
```
@@ -388,9 +380,8 @@ String res = Select.select().or(call1).or(call2).await();
```java
var handle =
- MyServiceClient.fromContext(ctx)
- .send()
- .myHandler(request, req -> req.idempotencyKey("abc123"));
+ Restate.serviceHandle(MyService.class)
+ .send(MyService::myHandler, request, InvocationOptions.idempotencyKey("abc123"));
```
### Attach to a Running Invocation
@@ -415,7 +406,7 @@ All handler inputs/outputs and state values use Jackson JSON serialization by de
### Custom Serde
-Implement `Serde` for custom serialization when Jackson defaults are not sufficient (binary payloads, non-JSON formats, or types with custom encoding). Pass the serde when declaring a `StateKey`, `DurablePromiseKey`, awakeable, or `ctx.run` call.
+Implement `Serde` for custom serialization when Jackson defaults are not sufficient (binary payloads, non-JSON formats, or types with custom encoding). Pass the serde when declaring a `StateKey`, `DurablePromiseKey`, awakeable, or `Restate.run` call.
---
@@ -431,6 +422,21 @@ Note: the Java SDK uses `TerminalException`, NOT `TerminalError` (which is used
Any other exception type causes automatic retries with exponential backoff. For retry policy configuration, refer to the retry guide.
+### TerminalException metadata
+
+You can attach a string metadata map to a `TerminalException`. The metadata is propagated to callers and accessible via `getMetadata()`. Requires Restate Server >= 1.6.
+
+```java
+throw new TerminalException("Something went wrong", Map.of("correlationId", correlationId));
+```
+
+Callers can read the metadata:
+
+```java
+Map metadata = e.getMetadata();
+String correlationId = metadata.get("correlationId");
+```
+
---
## SDK Clients (External Invocations)
@@ -441,27 +447,28 @@ Use `Client` to call Restate handlers from outside a Restate context (e.g., from
Client restateClient = Client.connect("http://localhost:8080");
// Request-response
-String result = MyServiceClient.fromClient(restateClient).myHandler("Hi");
+String result = restateClient.service(MyService.class).myHandler("Hi");
// One-way
-MyServiceClient.fromClient(restateClient).send().myHandler("Hi");
+restateClient.serviceHandle(MyService.class).send(MyService::myHandler, "Hi");
// Delayed
-MyServiceClient.fromClient(restateClient).send().myHandler("Hi", Duration.ofSeconds(1));
+restateClient
+ .serviceHandle(MyService.class)
+ .send(MyService::myHandler, "Hi", Duration.ofSeconds(1));
// With idempotency key
-MyObjectClient.fromClient(restateClient, "Mary")
- .send()
- .myHandler("Hi", opt -> opt.idempotencyKey("abc"));
+restateClient
+ .virtualObjectHandle(MyObject.class, "Mary")
+ .send(MyObject::myHandler, "Hi", InvocationOptions.idempotencyKey("abc"));
```
---
## Java-Specific Pitfalls
-- **Code generation creates typed client classes** (e.g., `MyServiceClient`) from `@Service`/`@VirtualObject`/`@Workflow` annotations. Use these for type-safe calls.
- **Use Restate's future combinators, NOT `CompletableFuture`.** Native Java futures break deterministic replay.
-- **Never use `Thread.sleep`, `Math.random()`, or `System.currentTimeMillis()`** -- use Restate context actions instead.
+- **Never use `Thread.sleep`, `Math.random()`, or `System.currentTimeMillis()`** -- use `Restate` SDK actions instead.
- **Never use global mutable variables for state** -- use Restate's K/V store for durable state.
- **For detailed API reference:** use the MCP server or JavaDocs.
@@ -490,7 +497,7 @@ class MyServiceTestMethod {
@Test
void testMyHandler(@RestateClient Client ingressClient) {
// Create the service client from the injected ingress client
- var client = MyServiceClient.fromClient(ingressClient);
+ var client = ingressClient.service(MyService.class);
// Send request to service and assert the response
var response = client.myHandler("Hi");
diff --git a/java/templates/java-maven-spring-boot/.aiassistant/rules/java-api-and-pitfalls.md b/java/templates/java-maven-spring-boot/.aiassistant/rules/java-api-and-pitfalls.md
index 3405ef1d..794c81c5 100644
--- a/java/templates/java-maven-spring-boot/.aiassistant/rules/java-api-and-pitfalls.md
+++ b/java/templates/java-maven-spring-boot/.aiassistant/rules/java-api-and-pitfalls.md
@@ -36,19 +36,16 @@ docker run -it docker.restate.dev/restatedev/restate-cli:latest invocations ls
**Gradle (build.gradle.kts):**
```kt
-// Annotation processor
-annotationProcessor("dev.restate:sdk-api-gen:2.4.1")
-
// For deploying as HTTP service
-implementation("dev.restate:sdk-java-http:2.4.1")
+implementation("dev.restate:sdk-java-http:2.9.0")
// Or for deploying using AWS Lambda
-implementation("dev.restate:sdk-java-lambda:2.4.1")
+implementation("dev.restate:sdk-java-lambda:2.9.0")
```
**Maven**:
```xml Java/Maven
- 2.4.1
+ 2.9.0
@@ -57,37 +54,20 @@ implementation("dev.restate:sdk-java-lambda:2.4.1")
sdk-java-http
${restate.version}
-
+
dev.restate
sdk-java-lambda
${restate.version}
-
-
-
- org.apache.maven.plugins
- maven-compiler-plugin
-
-
-
-
- dev.restate
- sdk-api-gen
- ${restate.version}
-
-
-
-
-
-
```
+On JDK 23+, pass `--enable-native-access=ALL-UNNAMED` as a JVM argument (or set `Enable-Native-Access: ALL-UNNAMED` in the JAR manifest) to silence the native-access warning printed at startup.
+
### Minimal Scaffold
```java
-import dev.restate.sdk.Context;
import dev.restate.sdk.annotation.Handler;
import dev.restate.sdk.annotation.Service;
import dev.restate.sdk.endpoint.Endpoint;
@@ -96,7 +76,7 @@ import dev.restate.sdk.http.vertx.RestateHttpServer;
@Service
public class MyService {
@Handler
- public String myHandler(Context ctx, String greeting) {
+ public String myHandler(String greeting) {
return greeting + "!";
}
@@ -120,9 +100,9 @@ curl localhost:8080/MyService/greet --json '"World"'
## Core Concepts
- Restate provides durable execution: if a handler crashes or the process restarts, Restate replays the handler from the last completed step, not from scratch.
-- All handlers receive a Context object (`ctx`) as their first parameter. Use ctx methods for all I/O and side effects.
+- Access all Restate functionality (state, calls, side effects, timers, ...) through the static methods on the `Restate` class (e.g. `Restate.state()`, `Restate.run(...)`, `Restate.sleep(...)`).
- Handlers take one optional JSON-serializable input parameter and return one JSON-serializable output.
-- Code generation produces typed client classes (e.g., `MyServiceClient`) from annotated service definitions.
+- Call other services through `Restate.service(...)` / `Restate.serviceHandle(...)` (and the virtual-object/workflow variants) with a method reference.
---
@@ -135,8 +115,7 @@ See minimal scaffold above.
### Virtual Object (Stateful, Keyed)
```java
-import dev.restate.sdk.ObjectContext;
-import dev.restate.sdk.SharedObjectContext;
+import dev.restate.sdk.Restate;
import dev.restate.sdk.annotation.Handler;
import dev.restate.sdk.annotation.Shared;
import dev.restate.sdk.annotation.VirtualObject;
@@ -147,14 +126,14 @@ import dev.restate.sdk.http.vertx.RestateHttpServer;
public class MyObject {
@Handler
- public String myHandler(ObjectContext ctx, String greeting) {
- String objectId = ctx.key();
+ public String myHandler(String greeting) {
+ String objectId = Restate.key();
return greeting + " " + objectId + "!";
}
@Shared
- public String myConcurrentHandler(SharedObjectContext ctx, String input) {
+ public String myConcurrentHandler(String input) {
return "my-output";
}
@@ -164,14 +143,12 @@ public class MyObject {
}
```
-- **Exclusive handlers** (`@Handler`): only one executes at a time per key. Use for writes. Receive `ObjectContext`.
-- **Shared handlers** (`@Shared`): run concurrently per key. Use for reads. Receive `SharedObjectContext`.
+- **Exclusive handlers** (`@Handler`): only one executes at a time per key. Use for writes. Have read/write state access via `Restate.state()`.
+- **Shared handlers** (`@Shared`): run concurrently per key. Use for reads. Have read-only state access.
### Workflow
```java
-import dev.restate.sdk.SharedWorkflowContext;
-import dev.restate.sdk.WorkflowContext;
import dev.restate.sdk.annotation.Shared;
import dev.restate.sdk.annotation.Workflow;
import dev.restate.sdk.endpoint.Endpoint;
@@ -181,7 +158,7 @@ import dev.restate.sdk.http.vertx.RestateHttpServer;
public class MyWorkflow {
@Workflow
- public String run(WorkflowContext ctx, String input) {
+ public String run(String input) {
// implement workflow logic here
@@ -189,7 +166,7 @@ public class MyWorkflow {
}
@Shared
- public String interactWithWorkflow(SharedWorkflowContext ctx, String input) {
+ public String interactWithWorkflow(String input) {
// implement interaction logic here
return "my result";
}
@@ -207,15 +184,16 @@ public class MyWorkflow {
## State Management
-Never use global variables for state -- it is not durable across restarts. Use `StateKey` with `ctx.get`/`ctx.set` instead (available on `ObjectContext` and `WorkflowContext`):
+Never use global variables for state -- it is not durable across restarts. Use `StateKey` with `Restate.state().get`/`Restate.state().set` instead:
```java
+var state = Restate.state();
StateKey STRING_STATE_KEY = StateKey.of("my-key", String.class);
-String stringState = ctx.get(STRING_STATE_KEY).orElse("my-default");
-ctx.set(STRING_STATE_KEY, "my-new-value");
-ctx.clear(STRING_STATE_KEY);
-ctx.clearAll();
-Collection keys = ctx.stateKeys();
+String stringState = state.get(STRING_STATE_KEY).orElse("my-default");
+state.set(STRING_STATE_KEY, "my-new-value");
+state.clear(STRING_STATE_KEY);
+state.clearAll();
+Collection keys = state.getAllKeys();
```
For generic types, use `TypeRef`:
@@ -232,26 +210,37 @@ private static final StateKey> ITEMS =
### Request-Response Calls
-Code generation creates typed client classes from annotated service definitions:
+Two client styles are available:
+
+- **Simple client** (`Restate.service(...)` / `Restate.virtualObject(...)` / `Restate.workflow(...)`): the call is awaited inline and returns the result directly. Use for straightforward request-response.
+- **Handle-based client** (`Restate.serviceHandle(...)` / `Restate.virtualObjectHandle(...)` / `Restate.workflowHandle(...)`, called with a method reference): returns a `DurableFuture` you await explicitly. Use it for invocation options (e.g. idempotency keys), timeouts, or concurrency.
```java
-String svcResponse = MyServiceClient.fromContext(ctx).myHandler(request).await();
-String objResponse = MyObjectClient.fromContext(ctx, objectKey).myHandler(request).await();
-String wfResponse = MyWorkflowClient.fromContext(ctx, workflowId).run(request).await();
+// Simple client: the call is awaited inline and returns the result directly.
+// Use this for straightforward request-response calls.
+String svcResponse = Restate.service(MyService.class).myHandler(request);
+String objResponse = Restate.virtualObject(MyObject.class, objectKey).myHandler(request);
+String wfResponse = Restate.workflow(MyWorkflow.class, workflowId).run(request);
+
+// Handle-based client: returns a DurableFuture that you await explicitly.
+// Use it for invocation options (e.g. an idempotency key), timeouts, or concurrency.
+// (virtualObjectHandle(...) / workflowHandle(...) work the same way.)
+String svcResult =
+ Restate.serviceHandle(MyService.class).call(MyService::myHandler, request).await();
```
### One-Way Calls (Fire-and-Forget)
```java
-MyServiceClient.fromContext(ctx).send().myHandler(request);
-MyObjectClient.fromContext(ctx, objectKey).send().myHandler(request);
-MyWorkflowClient.fromContext(ctx, workflowId).send().run(request);
+Restate.serviceHandle(MyService.class).send(MyService::myHandler, request);
+Restate.virtualObjectHandle(MyObject.class, objectKey).send(MyObject::myHandler, request);
+Restate.workflowHandle(MyWorkflow.class, workflowId).send(MyWorkflow::run, request);
```
### Delayed Calls
```java
-MyServiceClient.fromContext(ctx).send().myHandler(request, Duration.ofDays(5));
+Restate.serviceHandle(MyService.class).send(MyService::myHandler, request, Duration.ofDays(5));
```
### Generic Calls (String-Based Service/Method Names)
@@ -266,25 +255,26 @@ Target workflowTarget = Target.workflow("MyWorkflow", "wf-id", "run");
// Do the call
String response =
- ctx.call(Request.of(target, TypeTag.of(String.class), TypeTag.of(String.class), request))
+ Restate.call(
+ Request.of(target, TypeTag.of(String.class), TypeTag.of(String.class), request))
.await();
// Or send the message
-ctx.send(Request.of(target, TypeTag.of(String.class), TypeTag.of(String.class), request));
+Restate.send(Request.of(target, TypeTag.of(String.class), TypeTag.of(String.class), request));
```
---
-## Side Effects / ctx.run
+## Side Effects / Restate.run
-Never call external APIs, databases, or non-deterministic functions directly in a handler. Wrap them in `ctx.run`:
+Never call external APIs, databases, or non-deterministic functions directly in a handler. Wrap them in `Restate.run`:
```java
-// Wrap non-deterministic code in ctx.run
-String result = ctx.run("call external API", String.class, () -> callExternalAPI());
+// Wrap non-deterministic code in Restate.run
+String result = Restate.run("call external API", String.class, () -> callExternalAPI());
// Wrap with name for better tracing
-String namedResult = ctx.run("my-side-effect", String.class, () -> callExternalAPI());
+String namedResult = Restate.run("my-side-effect", String.class, () -> callExternalAPI());
```
- The first argument is a label used for observability and debugging.
@@ -296,21 +286,21 @@ String namedResult = ctx.run("my-side-effect", String.class, () -> callExternalA
## Deterministic Helpers
-Never use `Math.random()`, `System.currentTimeMillis()`, or `new Date()` directly -- they break deterministic replay. Use ctx helpers instead:
+Never use `Math.random()`, `System.currentTimeMillis()`, or `new Date()` directly -- they break deterministic replay. Use `Restate.random()` / `Restate.instantNow()` instead:
```java
-float value = ctx.random().nextFloat();
-UUID uuid = ctx.random().nextUUID();
+float value = Restate.random().nextFloat();
+UUID uuid = Restate.random().nextUUID();
```
---
## Durable Timers
-Never use `Thread.sleep`. Use `ctx.sleep` for durable delays that survive crashes and restarts:
+Never use `Thread.sleep`. Use `Restate.sleep` for durable delays that survive crashes and restarts:
```java
-ctx.sleep(Duration.ofHours(30));
+Restate.sleep(Duration.ofHours(30));
```
---
@@ -321,11 +311,11 @@ Awakeables pause execution until an external system signals completion:
```java
// Create awakeable
-Awakeable awakeable = ctx.awakeable(String.class);
+Awakeable awakeable = Restate.awakeable(String.class);
String awakeableId = awakeable.id();
// Send ID to external system
-ctx.run(() -> requestHumanReview(name, awakeableId));
+Restate.run("request-human-review", () -> requestHumanReview(name, awakeableId));
// Wait for result
String review = awakeable.await();
@@ -337,8 +327,8 @@ External systems can also resolve/reject via HTTP:
Or from another handler:
```java
-ctx.awakeableHandle(awakeableId).resolve(String.class, "Looks good!");
-ctx.awakeableHandle(awakeableId).reject("Cannot be reviewed");
+Restate.awakeableHandle(awakeableId).resolve(String.class, "Looks good!");
+Restate.awakeableHandle(awakeableId).reject("Cannot be reviewed");
```
---
@@ -350,10 +340,10 @@ Cross-handler signaling within a Workflow. No ID management needed.
```java
DurablePromiseKey REVIEW_PROMISE = DurablePromiseKey.of("review", String.class);
// Wait for promise
-String review = ctx.promise(REVIEW_PROMISE).future().await();
+String review = Restate.promise(REVIEW_PROMISE).future().await();
// Resolve promise from another handler
-ctx.promiseHandle(REVIEW_PROMISE).resolve(review);
+Restate.promiseHandle(REVIEW_PROMISE).resolve(review);
```
---
@@ -366,8 +356,10 @@ Use `DurableFuture` combinators, NOT `CompletableFuture`. Native combinators are
```java
// Wait for all to complete
-DurableFuture call1 = MyServiceClient.fromContext(ctx).myHandler("request1");
-DurableFuture call2 = MyServiceClient.fromContext(ctx).myHandler("request2");
+DurableFuture call1 =
+ Restate.serviceHandle(MyService.class).call(MyService::myHandler, "request1");
+DurableFuture call2 =
+ Restate.serviceHandle(MyService.class).call(MyService::myHandler, "request2");
DurableFuture.all(call1, call2).await();
```
@@ -388,9 +380,8 @@ String res = Select.select().or(call1).or(call2).await();
```java
var handle =
- MyServiceClient.fromContext(ctx)
- .send()
- .myHandler(request, req -> req.idempotencyKey("abc123"));
+ Restate.serviceHandle(MyService.class)
+ .send(MyService::myHandler, request, InvocationOptions.idempotencyKey("abc123"));
```
### Attach to a Running Invocation
@@ -415,7 +406,7 @@ All handler inputs/outputs and state values use Jackson JSON serialization by de
### Custom Serde
-Implement `Serde` for custom serialization when Jackson defaults are not sufficient (binary payloads, non-JSON formats, or types with custom encoding). Pass the serde when declaring a `StateKey`, `DurablePromiseKey`, awakeable, or `ctx.run` call.
+Implement `Serde` for custom serialization when Jackson defaults are not sufficient (binary payloads, non-JSON formats, or types with custom encoding). Pass the serde when declaring a `StateKey`, `DurablePromiseKey`, awakeable, or `Restate.run` call.
---
@@ -431,6 +422,21 @@ Note: the Java SDK uses `TerminalException`, NOT `TerminalError` (which is used
Any other exception type causes automatic retries with exponential backoff. For retry policy configuration, refer to the retry guide.
+### TerminalException metadata
+
+You can attach a string metadata map to a `TerminalException`. The metadata is propagated to callers and accessible via `getMetadata()`. Requires Restate Server >= 1.6.
+
+```java
+throw new TerminalException("Something went wrong", Map.of("correlationId", correlationId));
+```
+
+Callers can read the metadata:
+
+```java
+Map metadata = e.getMetadata();
+String correlationId = metadata.get("correlationId");
+```
+
---
## SDK Clients (External Invocations)
@@ -441,27 +447,28 @@ Use `Client` to call Restate handlers from outside a Restate context (e.g., from
Client restateClient = Client.connect("http://localhost:8080");
// Request-response
-String result = MyServiceClient.fromClient(restateClient).myHandler("Hi");
+String result = restateClient.service(MyService.class).myHandler("Hi");
// One-way
-MyServiceClient.fromClient(restateClient).send().myHandler("Hi");
+restateClient.serviceHandle(MyService.class).send(MyService::myHandler, "Hi");
// Delayed
-MyServiceClient.fromClient(restateClient).send().myHandler("Hi", Duration.ofSeconds(1));
+restateClient
+ .serviceHandle(MyService.class)
+ .send(MyService::myHandler, "Hi", Duration.ofSeconds(1));
// With idempotency key
-MyObjectClient.fromClient(restateClient, "Mary")
- .send()
- .myHandler("Hi", opt -> opt.idempotencyKey("abc"));
+restateClient
+ .virtualObjectHandle(MyObject.class, "Mary")
+ .send(MyObject::myHandler, "Hi", InvocationOptions.idempotencyKey("abc"));
```
---
## Java-Specific Pitfalls
-- **Code generation creates typed client classes** (e.g., `MyServiceClient`) from `@Service`/`@VirtualObject`/`@Workflow` annotations. Use these for type-safe calls.
- **Use Restate's future combinators, NOT `CompletableFuture`.** Native Java futures break deterministic replay.
-- **Never use `Thread.sleep`, `Math.random()`, or `System.currentTimeMillis()`** -- use Restate context actions instead.
+- **Never use `Thread.sleep`, `Math.random()`, or `System.currentTimeMillis()`** -- use `Restate` SDK actions instead.
- **Never use global mutable variables for state** -- use Restate's K/V store for durable state.
- **For detailed API reference:** use the MCP server or JavaDocs.
@@ -490,7 +497,7 @@ class MyServiceTestMethod {
@Test
void testMyHandler(@RestateClient Client ingressClient) {
// Create the service client from the injected ingress client
- var client = MyServiceClient.fromClient(ingressClient);
+ var client = ingressClient.service(MyService.class);
// Send request to service and assert the response
var response = client.myHandler("Hi");
diff --git a/java/templates/java-maven/.aiassistant/rules/java-api-and-pitfalls.md b/java/templates/java-maven/.aiassistant/rules/java-api-and-pitfalls.md
index 3405ef1d..794c81c5 100644
--- a/java/templates/java-maven/.aiassistant/rules/java-api-and-pitfalls.md
+++ b/java/templates/java-maven/.aiassistant/rules/java-api-and-pitfalls.md
@@ -36,19 +36,16 @@ docker run -it docker.restate.dev/restatedev/restate-cli:latest invocations ls
**Gradle (build.gradle.kts):**
```kt
-// Annotation processor
-annotationProcessor("dev.restate:sdk-api-gen:2.4.1")
-
// For deploying as HTTP service
-implementation("dev.restate:sdk-java-http:2.4.1")
+implementation("dev.restate:sdk-java-http:2.9.0")
// Or for deploying using AWS Lambda
-implementation("dev.restate:sdk-java-lambda:2.4.1")
+implementation("dev.restate:sdk-java-lambda:2.9.0")
```
**Maven**:
```xml Java/Maven
- 2.4.1
+ 2.9.0
@@ -57,37 +54,20 @@ implementation("dev.restate:sdk-java-lambda:2.4.1")
sdk-java-http
${restate.version}
-
+
dev.restate
sdk-java-lambda
${restate.version}
-
-
-
- org.apache.maven.plugins
- maven-compiler-plugin
-
-
-
-
- dev.restate
- sdk-api-gen
- ${restate.version}
-
-
-
-
-
-
```
+On JDK 23+, pass `--enable-native-access=ALL-UNNAMED` as a JVM argument (or set `Enable-Native-Access: ALL-UNNAMED` in the JAR manifest) to silence the native-access warning printed at startup.
+
### Minimal Scaffold
```java
-import dev.restate.sdk.Context;
import dev.restate.sdk.annotation.Handler;
import dev.restate.sdk.annotation.Service;
import dev.restate.sdk.endpoint.Endpoint;
@@ -96,7 +76,7 @@ import dev.restate.sdk.http.vertx.RestateHttpServer;
@Service
public class MyService {
@Handler
- public String myHandler(Context ctx, String greeting) {
+ public String myHandler(String greeting) {
return greeting + "!";
}
@@ -120,9 +100,9 @@ curl localhost:8080/MyService/greet --json '"World"'
## Core Concepts
- Restate provides durable execution: if a handler crashes or the process restarts, Restate replays the handler from the last completed step, not from scratch.
-- All handlers receive a Context object (`ctx`) as their first parameter. Use ctx methods for all I/O and side effects.
+- Access all Restate functionality (state, calls, side effects, timers, ...) through the static methods on the `Restate` class (e.g. `Restate.state()`, `Restate.run(...)`, `Restate.sleep(...)`).
- Handlers take one optional JSON-serializable input parameter and return one JSON-serializable output.
-- Code generation produces typed client classes (e.g., `MyServiceClient`) from annotated service definitions.
+- Call other services through `Restate.service(...)` / `Restate.serviceHandle(...)` (and the virtual-object/workflow variants) with a method reference.
---
@@ -135,8 +115,7 @@ See minimal scaffold above.
### Virtual Object (Stateful, Keyed)
```java
-import dev.restate.sdk.ObjectContext;
-import dev.restate.sdk.SharedObjectContext;
+import dev.restate.sdk.Restate;
import dev.restate.sdk.annotation.Handler;
import dev.restate.sdk.annotation.Shared;
import dev.restate.sdk.annotation.VirtualObject;
@@ -147,14 +126,14 @@ import dev.restate.sdk.http.vertx.RestateHttpServer;
public class MyObject {
@Handler
- public String myHandler(ObjectContext ctx, String greeting) {
- String objectId = ctx.key();
+ public String myHandler(String greeting) {
+ String objectId = Restate.key();
return greeting + " " + objectId + "!";
}
@Shared
- public String myConcurrentHandler(SharedObjectContext ctx, String input) {
+ public String myConcurrentHandler(String input) {
return "my-output";
}
@@ -164,14 +143,12 @@ public class MyObject {
}
```
-- **Exclusive handlers** (`@Handler`): only one executes at a time per key. Use for writes. Receive `ObjectContext`.
-- **Shared handlers** (`@Shared`): run concurrently per key. Use for reads. Receive `SharedObjectContext`.
+- **Exclusive handlers** (`@Handler`): only one executes at a time per key. Use for writes. Have read/write state access via `Restate.state()`.
+- **Shared handlers** (`@Shared`): run concurrently per key. Use for reads. Have read-only state access.
### Workflow
```java
-import dev.restate.sdk.SharedWorkflowContext;
-import dev.restate.sdk.WorkflowContext;
import dev.restate.sdk.annotation.Shared;
import dev.restate.sdk.annotation.Workflow;
import dev.restate.sdk.endpoint.Endpoint;
@@ -181,7 +158,7 @@ import dev.restate.sdk.http.vertx.RestateHttpServer;
public class MyWorkflow {
@Workflow
- public String run(WorkflowContext ctx, String input) {
+ public String run(String input) {
// implement workflow logic here
@@ -189,7 +166,7 @@ public class MyWorkflow {
}
@Shared
- public String interactWithWorkflow(SharedWorkflowContext ctx, String input) {
+ public String interactWithWorkflow(String input) {
// implement interaction logic here
return "my result";
}
@@ -207,15 +184,16 @@ public class MyWorkflow {
## State Management
-Never use global variables for state -- it is not durable across restarts. Use `StateKey` with `ctx.get`/`ctx.set` instead (available on `ObjectContext` and `WorkflowContext`):
+Never use global variables for state -- it is not durable across restarts. Use `StateKey` with `Restate.state().get`/`Restate.state().set` instead:
```java
+var state = Restate.state();
StateKey STRING_STATE_KEY = StateKey.of("my-key", String.class);
-String stringState = ctx.get(STRING_STATE_KEY).orElse("my-default");
-ctx.set(STRING_STATE_KEY, "my-new-value");
-ctx.clear(STRING_STATE_KEY);
-ctx.clearAll();
-Collection keys = ctx.stateKeys();
+String stringState = state.get(STRING_STATE_KEY).orElse("my-default");
+state.set(STRING_STATE_KEY, "my-new-value");
+state.clear(STRING_STATE_KEY);
+state.clearAll();
+Collection keys = state.getAllKeys();
```
For generic types, use `TypeRef`:
@@ -232,26 +210,37 @@ private static final StateKey> ITEMS =
### Request-Response Calls
-Code generation creates typed client classes from annotated service definitions:
+Two client styles are available:
+
+- **Simple client** (`Restate.service(...)` / `Restate.virtualObject(...)` / `Restate.workflow(...)`): the call is awaited inline and returns the result directly. Use for straightforward request-response.
+- **Handle-based client** (`Restate.serviceHandle(...)` / `Restate.virtualObjectHandle(...)` / `Restate.workflowHandle(...)`, called with a method reference): returns a `DurableFuture` you await explicitly. Use it for invocation options (e.g. idempotency keys), timeouts, or concurrency.
```java
-String svcResponse = MyServiceClient.fromContext(ctx).myHandler(request).await();
-String objResponse = MyObjectClient.fromContext(ctx, objectKey).myHandler(request).await();
-String wfResponse = MyWorkflowClient.fromContext(ctx, workflowId).run(request).await();
+// Simple client: the call is awaited inline and returns the result directly.
+// Use this for straightforward request-response calls.
+String svcResponse = Restate.service(MyService.class).myHandler(request);
+String objResponse = Restate.virtualObject(MyObject.class, objectKey).myHandler(request);
+String wfResponse = Restate.workflow(MyWorkflow.class, workflowId).run(request);
+
+// Handle-based client: returns a DurableFuture that you await explicitly.
+// Use it for invocation options (e.g. an idempotency key), timeouts, or concurrency.
+// (virtualObjectHandle(...) / workflowHandle(...) work the same way.)
+String svcResult =
+ Restate.serviceHandle(MyService.class).call(MyService::myHandler, request).await();
```
### One-Way Calls (Fire-and-Forget)
```java
-MyServiceClient.fromContext(ctx).send().myHandler(request);
-MyObjectClient.fromContext(ctx, objectKey).send().myHandler(request);
-MyWorkflowClient.fromContext(ctx, workflowId).send().run(request);
+Restate.serviceHandle(MyService.class).send(MyService::myHandler, request);
+Restate.virtualObjectHandle(MyObject.class, objectKey).send(MyObject::myHandler, request);
+Restate.workflowHandle(MyWorkflow.class, workflowId).send(MyWorkflow::run, request);
```
### Delayed Calls
```java
-MyServiceClient.fromContext(ctx).send().myHandler(request, Duration.ofDays(5));
+Restate.serviceHandle(MyService.class).send(MyService::myHandler, request, Duration.ofDays(5));
```
### Generic Calls (String-Based Service/Method Names)
@@ -266,25 +255,26 @@ Target workflowTarget = Target.workflow("MyWorkflow", "wf-id", "run");
// Do the call
String response =
- ctx.call(Request.of(target, TypeTag.of(String.class), TypeTag.of(String.class), request))
+ Restate.call(
+ Request.of(target, TypeTag.of(String.class), TypeTag.of(String.class), request))
.await();
// Or send the message
-ctx.send(Request.of(target, TypeTag.of(String.class), TypeTag.of(String.class), request));
+Restate.send(Request.of(target, TypeTag.of(String.class), TypeTag.of(String.class), request));
```
---
-## Side Effects / ctx.run
+## Side Effects / Restate.run
-Never call external APIs, databases, or non-deterministic functions directly in a handler. Wrap them in `ctx.run`:
+Never call external APIs, databases, or non-deterministic functions directly in a handler. Wrap them in `Restate.run`:
```java
-// Wrap non-deterministic code in ctx.run
-String result = ctx.run("call external API", String.class, () -> callExternalAPI());
+// Wrap non-deterministic code in Restate.run
+String result = Restate.run("call external API", String.class, () -> callExternalAPI());
// Wrap with name for better tracing
-String namedResult = ctx.run("my-side-effect", String.class, () -> callExternalAPI());
+String namedResult = Restate.run("my-side-effect", String.class, () -> callExternalAPI());
```
- The first argument is a label used for observability and debugging.
@@ -296,21 +286,21 @@ String namedResult = ctx.run("my-side-effect", String.class, () -> callExternalA
## Deterministic Helpers
-Never use `Math.random()`, `System.currentTimeMillis()`, or `new Date()` directly -- they break deterministic replay. Use ctx helpers instead:
+Never use `Math.random()`, `System.currentTimeMillis()`, or `new Date()` directly -- they break deterministic replay. Use `Restate.random()` / `Restate.instantNow()` instead:
```java
-float value = ctx.random().nextFloat();
-UUID uuid = ctx.random().nextUUID();
+float value = Restate.random().nextFloat();
+UUID uuid = Restate.random().nextUUID();
```
---
## Durable Timers
-Never use `Thread.sleep`. Use `ctx.sleep` for durable delays that survive crashes and restarts:
+Never use `Thread.sleep`. Use `Restate.sleep` for durable delays that survive crashes and restarts:
```java
-ctx.sleep(Duration.ofHours(30));
+Restate.sleep(Duration.ofHours(30));
```
---
@@ -321,11 +311,11 @@ Awakeables pause execution until an external system signals completion:
```java
// Create awakeable
-Awakeable awakeable = ctx.awakeable(String.class);
+Awakeable awakeable = Restate.awakeable(String.class);
String awakeableId = awakeable.id();
// Send ID to external system
-ctx.run(() -> requestHumanReview(name, awakeableId));
+Restate.run("request-human-review", () -> requestHumanReview(name, awakeableId));
// Wait for result
String review = awakeable.await();
@@ -337,8 +327,8 @@ External systems can also resolve/reject via HTTP:
Or from another handler:
```java
-ctx.awakeableHandle(awakeableId).resolve(String.class, "Looks good!");
-ctx.awakeableHandle(awakeableId).reject("Cannot be reviewed");
+Restate.awakeableHandle(awakeableId).resolve(String.class, "Looks good!");
+Restate.awakeableHandle(awakeableId).reject("Cannot be reviewed");
```
---
@@ -350,10 +340,10 @@ Cross-handler signaling within a Workflow. No ID management needed.
```java
DurablePromiseKey REVIEW_PROMISE = DurablePromiseKey.of("review", String.class);
// Wait for promise
-String review = ctx.promise(REVIEW_PROMISE).future().await();
+String review = Restate.promise(REVIEW_PROMISE).future().await();
// Resolve promise from another handler
-ctx.promiseHandle(REVIEW_PROMISE).resolve(review);
+Restate.promiseHandle(REVIEW_PROMISE).resolve(review);
```
---
@@ -366,8 +356,10 @@ Use `DurableFuture` combinators, NOT `CompletableFuture`. Native combinators are
```java
// Wait for all to complete
-DurableFuture call1 = MyServiceClient.fromContext(ctx).myHandler("request1");
-DurableFuture call2 = MyServiceClient.fromContext(ctx).myHandler("request2");
+DurableFuture call1 =
+ Restate.serviceHandle(MyService.class).call(MyService::myHandler, "request1");
+DurableFuture call2 =
+ Restate.serviceHandle(MyService.class).call(MyService::myHandler, "request2");
DurableFuture.all(call1, call2).await();
```
@@ -388,9 +380,8 @@ String res = Select.select().or(call1).or(call2).await();
```java
var handle =
- MyServiceClient.fromContext(ctx)
- .send()
- .myHandler(request, req -> req.idempotencyKey("abc123"));
+ Restate.serviceHandle(MyService.class)
+ .send(MyService::myHandler, request, InvocationOptions.idempotencyKey("abc123"));
```
### Attach to a Running Invocation
@@ -415,7 +406,7 @@ All handler inputs/outputs and state values use Jackson JSON serialization by de
### Custom Serde
-Implement `Serde` for custom serialization when Jackson defaults are not sufficient (binary payloads, non-JSON formats, or types with custom encoding). Pass the serde when declaring a `StateKey`, `DurablePromiseKey`, awakeable, or `ctx.run` call.
+Implement `Serde` for custom serialization when Jackson defaults are not sufficient (binary payloads, non-JSON formats, or types with custom encoding). Pass the serde when declaring a `StateKey`, `DurablePromiseKey`, awakeable, or `Restate.run` call.
---
@@ -431,6 +422,21 @@ Note: the Java SDK uses `TerminalException`, NOT `TerminalError` (which is used
Any other exception type causes automatic retries with exponential backoff. For retry policy configuration, refer to the retry guide.
+### TerminalException metadata
+
+You can attach a string metadata map to a `TerminalException`. The metadata is propagated to callers and accessible via `getMetadata()`. Requires Restate Server >= 1.6.
+
+```java
+throw new TerminalException("Something went wrong", Map.of("correlationId", correlationId));
+```
+
+Callers can read the metadata:
+
+```java
+Map metadata = e.getMetadata();
+String correlationId = metadata.get("correlationId");
+```
+
---
## SDK Clients (External Invocations)
@@ -441,27 +447,28 @@ Use `Client` to call Restate handlers from outside a Restate context (e.g., from
Client restateClient = Client.connect("http://localhost:8080");
// Request-response
-String result = MyServiceClient.fromClient(restateClient).myHandler("Hi");
+String result = restateClient.service(MyService.class).myHandler("Hi");
// One-way
-MyServiceClient.fromClient(restateClient).send().myHandler("Hi");
+restateClient.serviceHandle(MyService.class).send(MyService::myHandler, "Hi");
// Delayed
-MyServiceClient.fromClient(restateClient).send().myHandler("Hi", Duration.ofSeconds(1));
+restateClient
+ .serviceHandle(MyService.class)
+ .send(MyService::myHandler, "Hi", Duration.ofSeconds(1));
// With idempotency key
-MyObjectClient.fromClient(restateClient, "Mary")
- .send()
- .myHandler("Hi", opt -> opt.idempotencyKey("abc"));
+restateClient
+ .virtualObjectHandle(MyObject.class, "Mary")
+ .send(MyObject::myHandler, "Hi", InvocationOptions.idempotencyKey("abc"));
```
---
## Java-Specific Pitfalls
-- **Code generation creates typed client classes** (e.g., `MyServiceClient`) from `@Service`/`@VirtualObject`/`@Workflow` annotations. Use these for type-safe calls.
- **Use Restate's future combinators, NOT `CompletableFuture`.** Native Java futures break deterministic replay.
-- **Never use `Thread.sleep`, `Math.random()`, or `System.currentTimeMillis()`** -- use Restate context actions instead.
+- **Never use `Thread.sleep`, `Math.random()`, or `System.currentTimeMillis()`** -- use `Restate` SDK actions instead.
- **Never use global mutable variables for state** -- use Restate's K/V store for durable state.
- **For detailed API reference:** use the MCP server or JavaDocs.
@@ -490,7 +497,7 @@ class MyServiceTestMethod {
@Test
void testMyHandler(@RestateClient Client ingressClient) {
// Create the service client from the injected ingress client
- var client = MyServiceClient.fromClient(ingressClient);
+ var client = ingressClient.service(MyService.class);
// Send request to service and assert the response
var response = client.myHandler("Hi");