diff --git a/.tools/update_jvm_examples.sh b/.tools/update_jvm_examples.sh index aaa24b30..c63a72cf 100755 --- a/.tools/update_jvm_examples.sh +++ b/.tools/update_jvm_examples.sh @@ -29,13 +29,9 @@ function search_and_replace_version_maven() { mvn -B versions:set-property -Dproperty=restate.version -DnewVersion=$NEW_VERSION # If this is a template directory and has existing agents documentation, update it - if [[ "$project_dir" == *"/templates/"* ]] && [ -f "./.cursor/rules/AGENTS.md" ]; then - echo "Updating agents documentation for template in $project_dir" - wget -O "./.cursor/rules/AGENTS.md" https://docs.restate.dev/develop/java/agents.md - fi - if [[ "$project_dir" == *"/templates/"* ]] && [ -f "./.claude/CLAUDE.md" ]; then + if [[ "$project_dir" == *"/templates/"* ]] && [ -f "./AGENTS.md" ]; then echo "Updating agents documentation for template in $project_dir" - wget -O "./.claude/CLAUDE.md" https://docs.restate.dev/develop/java/agents.md + wget -O "./AGENTS.md" https://docs.restate.dev/develop/java/agents.md fi popd } diff --git a/java/templates/java-gradle/.ai/mcp/mcp.json b/java/templates/java-gradle/.ai/mcp/mcp.json new file mode 100644 index 00000000..78a21c5a --- /dev/null +++ b/java/templates/java-gradle/.ai/mcp/mcp.json @@ -0,0 +1,7 @@ +{ + "mcpServers": { + "restate-docs": { + "url": "https://docs.restate.dev/mcp" + } + } +} \ No newline at end of file diff --git a/java/templates/java-gradle/.aiassistant/rules/restate.md b/java/templates/java-gradle/.aiassistant/rules/restate.md new file mode 120000 index 00000000..b7e6491d --- /dev/null +++ b/java/templates/java-gradle/.aiassistant/rules/restate.md @@ -0,0 +1 @@ +../../AGENTS.md \ No newline at end of file diff --git a/java/templates/java-gradle/.claude/CLAUDE.md b/java/templates/java-gradle/.claude/CLAUDE.md deleted file mode 100644 index a7454322..00000000 --- a/java/templates/java-gradle/.claude/CLAUDE.md +++ /dev/null @@ -1,408 +0,0 @@ -# Restate Java SDK Rules - -## Core Concepts - -* Restate provides durable execution: code automatically stores completed steps and resumes from where it left off on failures -* All handlers receive a `Context`/`ObjectContext`/`WorkflowContext`/`SharedObjectContext`/`SharedWorkflowContext` object as the first argument -* Handlers can take typed inputs and return typed outputs using Java classes and Jackson serialization - -## Service Types - -### Basic Services - -```java Java {"CODE_LOAD::java/src/main/java/develop/MyService.java#here"} theme={null} -import dev.restate.sdk.Context; -import dev.restate.sdk.annotation.Handler; -import dev.restate.sdk.annotation.Service; -import dev.restate.sdk.endpoint.Endpoint; -import dev.restate.sdk.http.vertx.RestateHttpServer; - -@Service -public class MyService { - @Handler - public String myHandler(Context ctx, String greeting) { - return greeting + "!"; - } - - public static void main(String[] args) { - RestateHttpServer.listen(Endpoint.bind(new MyService())); - } -} -``` - -### Virtual Objects (Stateful, Key-Addressable) - -```java Java {"CODE_LOAD::java/src/main/java/develop/MyObject.java#here"} theme={null} -import dev.restate.sdk.ObjectContext; -import dev.restate.sdk.SharedObjectContext; -import dev.restate.sdk.annotation.Handler; -import dev.restate.sdk.annotation.Shared; -import dev.restate.sdk.annotation.VirtualObject; -import dev.restate.sdk.endpoint.Endpoint; -import dev.restate.sdk.http.vertx.RestateHttpServer; - -@VirtualObject -public class MyObject { - - @Handler - public String myHandler(ObjectContext ctx, String greeting) { - String objectId = ctx.key(); - - return greeting + " " + objectId + "!"; - } - - @Shared - public String myConcurrentHandler(SharedObjectContext ctx, String input) { - return "my-output"; - } - - public static void main(String[] args) { - RestateHttpServer.listen(Endpoint.bind(new MyObject())); - } -} -``` - -### Workflows - -```java Java {"CODE_LOAD::java/src/main/java/develop/MyWorkflow.java#here"} theme={null} -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; -import dev.restate.sdk.http.vertx.RestateHttpServer; - -@Workflow -public class MyWorkflow { - - @Workflow - public String run(WorkflowContext ctx, String input) { - - // implement workflow logic here - - return "success"; - } - - @Shared - public String interactWithWorkflow(SharedWorkflowContext ctx, String input) { - // implement interaction logic here - return "my result"; - } - - public static void main(String[] args) { - RestateHttpServer.listen(Endpoint.bind(new MyWorkflow())); - } -} -``` - -## Context Operations - -### State Management (Virtual Objects & Workflows only) - -❌ Never use static variables - not durable, lost across replicas. -✅ Use `ctx.get()` and `ctx.set()` - durable and scoped to the object's key. - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#state"} theme={null} -// Get state keys -Collection keys = ctx.stateKeys(); - -// Get state -StateKey STRING_STATE_KEY = StateKey.of("my-key", String.class); -String stringState = ctx.get(STRING_STATE_KEY).orElse("my-default"); - -StateKey INT_STATE_KEY = StateKey.of("count", Integer.class); -int count = ctx.get(INT_STATE_KEY).orElse(0); - -// Set state -ctx.set(STRING_STATE_KEY, "my-new-value"); -ctx.set(INT_STATE_KEY, count + 1); - -// Clear state -ctx.clear(STRING_STATE_KEY); -ctx.clearAll(); -``` - -### Service Communication - -#### Request-Response - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#service_calls"} theme={null} -// Call a Service -String svcResponse = MyServiceClient.fromContext(ctx).myHandler(request).await(); - -// Call a Virtual Object -String objResponse = MyObjectClient.fromContext(ctx, objectKey).myHandler(request).await(); - -// Call a Workflow -String wfResponse = MyWorkflowClient.fromContext(ctx, workflowId).run(request).await(); -``` - -#### Generic Calls - -Call a service without using the generated client, but just String names. - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#generic_calls"} theme={null} -// Generic service call -Target target = Target.service("MyService", "myHandler"); -String response = - ctx.call(Request.of(target, TypeTag.of(String.class), TypeTag.of(String.class), request)) - .await(); - -// Generic object call -Target objectTarget = Target.virtualObject("MyObject", "object-key", "myHandler"); -String objResponse = - ctx.call( - Request.of( - objectTarget, TypeTag.of(String.class), TypeTag.of(String.class), request)) - .await(); - -// Generic workflow call -Target workflowTarget = Target.workflow("MyWorkflow", "wf-id", "run"); -String wfResponse = - ctx.call( - Request.of( - workflowTarget, TypeTag.of(String.class), TypeTag.of(String.class), request)) - .await(); -``` - -#### One-Way Messages - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#sending_messages"} theme={null} -// Call a Service -MyServiceClient.fromContext(ctx).send().myHandler(request); - -// Call a Virtual Object -MyObjectClient.fromContext(ctx, objectKey).send().myHandler(request); - -// Call a Workflow -MyWorkflowClient.fromContext(ctx, workflowId).send().run(request); -``` - -#### Delayed Messages - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#delayed_messages"} theme={null} -MyServiceClient.fromContext(ctx).send().myHandler(request, Duration.ofDays(5)); -``` - -#### With Idempotency Key - -```java theme={null} -Client restateClient = Client.connect("http://localhost:8080"); -MyServiceClient.fromClient(restateClient) - .send() - .myHandler("Hi", opt -> opt.idempotencyKey("my-key")); -``` - -### Run Actions or Side Effects (Non-Deterministic Operations) - -❌ Never call external APIs/DBs directly - will re-execute during replay, causing duplicates. -✅ Wrap in `ctx.run()` - Restate journals the result; runs only once. - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#durable_steps"} theme={null} -// Wrap non-deterministic code in ctx.run -String result = ctx.run("call external API", String.class, () -> callExternalAPI()); - -// Wrap with name for better tracing -String namedResult = ctx.run("my-side-effect", String.class, () -> callExternalAPI()); -``` - -### Deterministic randoms and time - -❌ Never use `Math.random()` - non-deterministic and breaks replay logic. -✅ Use `ctx.random()` or `ctx.uuid()` - Restate journals the result for deterministic replay. - -❌ Never use `System.currentTimeMillis()`, `new Date()` - returns different values during replay. -✅ Use `ctx.timer()` - Restate records and replays the same timestamp. - -### Durable Timers and Sleep - -❌ Never use `Thread.sleep()` or `CompletableFuture.delayedExecutor()` - not durable, lost on restarts. -✅ Use `ctx.sleep()` - durable timer that survives failures. - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#durable_timers"} theme={null} -// Sleep -ctx.sleep(Duration.ofSeconds(30)); - -// Schedule delayed call (different from sleep + send) -Target target = Target.service("MyService", "myHandler"); -ctx.send( - Request.of(target, TypeTag.of(String.class), TypeTag.of(String.class), "Hi"), - Duration.ofHours(5)); -``` - -### Awakeables (External Events) - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#awakeables"} theme={null} -// Create awakeable -Awakeable awakeable = ctx.awakeable(String.class); -String awakeableId = awakeable.id(); - -// Send ID to external system -ctx.run(() -> requestHumanReview(name, awakeableId)); - -// Wait for result -String review = awakeable.await(); - -// Resolve from another handler -ctx.awakeableHandle(awakeableId).resolve(String.class, "Looks good!"); - -// Reject from another handler -ctx.awakeableHandle(awakeableId).reject("Cannot be reviewed"); -``` - -### Durable Promises (Workflows only) - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#workflow_promises"} theme={null} -DurablePromiseKey REVIEW_PROMISE = DurablePromiseKey.of("review", String.class); -// Wait for promise -String review = ctx.promise(REVIEW_PROMISE).future().await(); - -// Resolve promise from another handler -ctx.promiseHandle(REVIEW_PROMISE).resolve(review); -``` - -## Concurrency - -Always use Restate combinators (`DurableFuture.all`, `DurableFuture.any`) instead of Java's native `CompletableFuture` methods - they journal execution order for deterministic replay. - -### `DurableFuture.all()` - Wait for All - -Returns when all futures complete. Use to wait for multiple operations to finish. - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#combine_all"} theme={null} -// Wait for all to complete -DurableFuture call1 = MyServiceClient.fromContext(ctx).myHandler("request1"); -DurableFuture call2 = MyServiceClient.fromContext(ctx).myHandler("request2"); - -DurableFuture.all(call1, call2).await(); -``` - -### `DurableFuture.any()` - First Successful Result - -Returns the first successful result, ignoring rejections until all fail. - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#combine_any"} theme={null} -// Wait for any to complete -int indexCompleted = DurableFuture.any(call1, call2).await(); -``` - -### Invocation Management - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#cancel"} theme={null} -var handle = - MyServiceClient.fromContext(ctx) - .send() - .myHandler(request, req -> req.idempotencyKey("abc123")); -var response = handle.attach().await(); -// Cancel invocation -handle.cancel(); -``` - -## Serialization - -### Default (Jackson JSON) - -By default, Java SDK uses Jackson for JSON serialization with POJOs. - -```java Java {"CODE_LOAD::java/src/main/java/develop/SerializationExample.java#state_keys"} theme={null} -// Primitive types -var myString = StateKey.of("myString", String.class); -// Generic types need TypeRef (similar to Jackson's TypeReference) -var myMap = StateKey.of("myMap", TypeTag.of(new TypeRef>() {})); -``` - -### Custom Serialization - -```java {"CODE_LOAD::java/src/main/java/develop/SerializationExample.java#customserdes"} theme={null} -class MyPersonSerde implements Serde { - @Override - public Slice serialize(Person person) { - // convert value to a byte array, then wrap in a Slice - return Slice.wrap(person.toBytes()); - } - - @Override - public Person deserialize(Slice slice) { - // convert value to Person - return Person.fromBytes(slice.toByteArray()); - } -} -``` - -And then use it, for example, in combination with `ctx.run`: - -```java {"CODE_LOAD::java/src/main/java/develop/SerializationExample.java#use_person_serde"} theme={null} -ctx.run(new MyPersonSerde(), () -> new Person()); -``` - -## Error Handling - -Restate retries failures indefinitely by default. For permanent business-logic failures (invalid input, declined payment), use TerminalException to stop retries immediately. - -### Terminal Errors (No Retry) - -```java Java {"CODE_LOAD::java/src/main/java/develop/ErrorHandling.java#here"} theme={null} -throw new TerminalException(500, "Something went wrong"); -``` - -### Retryable Errors - -```java theme={null} -// Any other thrown exception will be retried -throw new RuntimeException("Temporary failure - will retry"); -``` - -## Testing - -```java Java {"CODE_LOAD::java/src/main/java/develop/MyServiceTestMethod.java"} theme={null} -package develop; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import dev.restate.client.Client; -import dev.restate.sdk.testing.BindService; -import dev.restate.sdk.testing.RestateClient; -import dev.restate.sdk.testing.RestateTest; -import org.junit.jupiter.api.Test; - -@RestateTest -class MyServiceTestMethod { - - @BindService MyService service = new MyService(); - - @Test - void testMyHandler(@RestateClient Client ingressClient) { - // Create the service client from the injected ingress client - var client = MyServiceClient.fromClient(ingressClient); - - // Send request to service and assert the response - var response = client.myHandler("Hi"); - assertEquals(response, "Hi!"); - } -} -``` - -## SDK Clients (External Invocations) - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Clients.java#here"} theme={null} -Client restateClient = Client.connect("http://localhost:8080"); - -// Request-response -String result = MyServiceClient.fromClient(restateClient).myHandler("Hi"); - -// One-way -MyServiceClient.fromClient(restateClient).send().myHandler("Hi"); - -// Delayed -MyServiceClient.fromClient(restateClient).send().myHandler("Hi", Duration.ofSeconds(1)); - -// With idempotency key -MyObjectClient.fromClient(restateClient, "Mary") - .send() - .myHandler("Hi", opt -> opt.idempotencyKey("abc")); -``` - - ---- - -> To find navigation and other pages in this documentation, fetch the llms.txt file at: https://docs.restate.dev/llms.txt \ No newline at end of file diff --git a/java/templates/java-gradle/.claude/CLAUDE.md b/java/templates/java-gradle/.claude/CLAUDE.md new file mode 120000 index 00000000..be77ac83 --- /dev/null +++ b/java/templates/java-gradle/.claude/CLAUDE.md @@ -0,0 +1 @@ +../AGENTS.md \ No newline at end of file diff --git a/java/templates/java-gradle/.cursor/mcp.json b/java/templates/java-gradle/.cursor/mcp.json deleted file mode 100644 index 78a21c5a..00000000 --- a/java/templates/java-gradle/.cursor/mcp.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "mcpServers": { - "restate-docs": { - "url": "https://docs.restate.dev/mcp" - } - } -} \ No newline at end of file diff --git a/java/templates/java-gradle/.cursor/mcp.json b/java/templates/java-gradle/.cursor/mcp.json new file mode 120000 index 00000000..3aa24b56 --- /dev/null +++ b/java/templates/java-gradle/.cursor/mcp.json @@ -0,0 +1 @@ +../.ai/mcp/mcp.json \ No newline at end of file diff --git a/java/templates/java-gradle/.junie/guidelines.md b/java/templates/java-gradle/.junie/guidelines.md new file mode 120000 index 00000000..be77ac83 --- /dev/null +++ b/java/templates/java-gradle/.junie/guidelines.md @@ -0,0 +1 @@ +../AGENTS.md \ No newline at end of file diff --git a/java/templates/java-gradle/.junie/mcp/mcp.json b/java/templates/java-gradle/.junie/mcp/mcp.json new file mode 120000 index 00000000..f454b32d --- /dev/null +++ b/java/templates/java-gradle/.junie/mcp/mcp.json @@ -0,0 +1 @@ +../../.ai/mcp/mcp.json \ No newline at end of file diff --git a/java/templates/java-gradle/.cursor/rules/AGENTS.md b/java/templates/java-gradle/AGENTS.md similarity index 100% rename from java/templates/java-gradle/.cursor/rules/AGENTS.md rename to java/templates/java-gradle/AGENTS.md diff --git a/java/templates/java-maven-quarkus/.ai/mcp/mcp.json b/java/templates/java-maven-quarkus/.ai/mcp/mcp.json new file mode 100644 index 00000000..78a21c5a --- /dev/null +++ b/java/templates/java-maven-quarkus/.ai/mcp/mcp.json @@ -0,0 +1,7 @@ +{ + "mcpServers": { + "restate-docs": { + "url": "https://docs.restate.dev/mcp" + } + } +} \ No newline at end of file diff --git a/java/templates/java-maven-quarkus/.aiassistant/rules/restate.md b/java/templates/java-maven-quarkus/.aiassistant/rules/restate.md new file mode 120000 index 00000000..b7e6491d --- /dev/null +++ b/java/templates/java-maven-quarkus/.aiassistant/rules/restate.md @@ -0,0 +1 @@ +../../AGENTS.md \ No newline at end of file diff --git a/java/templates/java-maven-quarkus/.claude/CLAUDE.md b/java/templates/java-maven-quarkus/.claude/CLAUDE.md deleted file mode 100644 index a7454322..00000000 --- a/java/templates/java-maven-quarkus/.claude/CLAUDE.md +++ /dev/null @@ -1,408 +0,0 @@ -# Restate Java SDK Rules - -## Core Concepts - -* Restate provides durable execution: code automatically stores completed steps and resumes from where it left off on failures -* All handlers receive a `Context`/`ObjectContext`/`WorkflowContext`/`SharedObjectContext`/`SharedWorkflowContext` object as the first argument -* Handlers can take typed inputs and return typed outputs using Java classes and Jackson serialization - -## Service Types - -### Basic Services - -```java Java {"CODE_LOAD::java/src/main/java/develop/MyService.java#here"} theme={null} -import dev.restate.sdk.Context; -import dev.restate.sdk.annotation.Handler; -import dev.restate.sdk.annotation.Service; -import dev.restate.sdk.endpoint.Endpoint; -import dev.restate.sdk.http.vertx.RestateHttpServer; - -@Service -public class MyService { - @Handler - public String myHandler(Context ctx, String greeting) { - return greeting + "!"; - } - - public static void main(String[] args) { - RestateHttpServer.listen(Endpoint.bind(new MyService())); - } -} -``` - -### Virtual Objects (Stateful, Key-Addressable) - -```java Java {"CODE_LOAD::java/src/main/java/develop/MyObject.java#here"} theme={null} -import dev.restate.sdk.ObjectContext; -import dev.restate.sdk.SharedObjectContext; -import dev.restate.sdk.annotation.Handler; -import dev.restate.sdk.annotation.Shared; -import dev.restate.sdk.annotation.VirtualObject; -import dev.restate.sdk.endpoint.Endpoint; -import dev.restate.sdk.http.vertx.RestateHttpServer; - -@VirtualObject -public class MyObject { - - @Handler - public String myHandler(ObjectContext ctx, String greeting) { - String objectId = ctx.key(); - - return greeting + " " + objectId + "!"; - } - - @Shared - public String myConcurrentHandler(SharedObjectContext ctx, String input) { - return "my-output"; - } - - public static void main(String[] args) { - RestateHttpServer.listen(Endpoint.bind(new MyObject())); - } -} -``` - -### Workflows - -```java Java {"CODE_LOAD::java/src/main/java/develop/MyWorkflow.java#here"} theme={null} -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; -import dev.restate.sdk.http.vertx.RestateHttpServer; - -@Workflow -public class MyWorkflow { - - @Workflow - public String run(WorkflowContext ctx, String input) { - - // implement workflow logic here - - return "success"; - } - - @Shared - public String interactWithWorkflow(SharedWorkflowContext ctx, String input) { - // implement interaction logic here - return "my result"; - } - - public static void main(String[] args) { - RestateHttpServer.listen(Endpoint.bind(new MyWorkflow())); - } -} -``` - -## Context Operations - -### State Management (Virtual Objects & Workflows only) - -❌ Never use static variables - not durable, lost across replicas. -✅ Use `ctx.get()` and `ctx.set()` - durable and scoped to the object's key. - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#state"} theme={null} -// Get state keys -Collection keys = ctx.stateKeys(); - -// Get state -StateKey STRING_STATE_KEY = StateKey.of("my-key", String.class); -String stringState = ctx.get(STRING_STATE_KEY).orElse("my-default"); - -StateKey INT_STATE_KEY = StateKey.of("count", Integer.class); -int count = ctx.get(INT_STATE_KEY).orElse(0); - -// Set state -ctx.set(STRING_STATE_KEY, "my-new-value"); -ctx.set(INT_STATE_KEY, count + 1); - -// Clear state -ctx.clear(STRING_STATE_KEY); -ctx.clearAll(); -``` - -### Service Communication - -#### Request-Response - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#service_calls"} theme={null} -// Call a Service -String svcResponse = MyServiceClient.fromContext(ctx).myHandler(request).await(); - -// Call a Virtual Object -String objResponse = MyObjectClient.fromContext(ctx, objectKey).myHandler(request).await(); - -// Call a Workflow -String wfResponse = MyWorkflowClient.fromContext(ctx, workflowId).run(request).await(); -``` - -#### Generic Calls - -Call a service without using the generated client, but just String names. - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#generic_calls"} theme={null} -// Generic service call -Target target = Target.service("MyService", "myHandler"); -String response = - ctx.call(Request.of(target, TypeTag.of(String.class), TypeTag.of(String.class), request)) - .await(); - -// Generic object call -Target objectTarget = Target.virtualObject("MyObject", "object-key", "myHandler"); -String objResponse = - ctx.call( - Request.of( - objectTarget, TypeTag.of(String.class), TypeTag.of(String.class), request)) - .await(); - -// Generic workflow call -Target workflowTarget = Target.workflow("MyWorkflow", "wf-id", "run"); -String wfResponse = - ctx.call( - Request.of( - workflowTarget, TypeTag.of(String.class), TypeTag.of(String.class), request)) - .await(); -``` - -#### One-Way Messages - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#sending_messages"} theme={null} -// Call a Service -MyServiceClient.fromContext(ctx).send().myHandler(request); - -// Call a Virtual Object -MyObjectClient.fromContext(ctx, objectKey).send().myHandler(request); - -// Call a Workflow -MyWorkflowClient.fromContext(ctx, workflowId).send().run(request); -``` - -#### Delayed Messages - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#delayed_messages"} theme={null} -MyServiceClient.fromContext(ctx).send().myHandler(request, Duration.ofDays(5)); -``` - -#### With Idempotency Key - -```java theme={null} -Client restateClient = Client.connect("http://localhost:8080"); -MyServiceClient.fromClient(restateClient) - .send() - .myHandler("Hi", opt -> opt.idempotencyKey("my-key")); -``` - -### Run Actions or Side Effects (Non-Deterministic Operations) - -❌ Never call external APIs/DBs directly - will re-execute during replay, causing duplicates. -✅ Wrap in `ctx.run()` - Restate journals the result; runs only once. - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#durable_steps"} theme={null} -// Wrap non-deterministic code in ctx.run -String result = ctx.run("call external API", String.class, () -> callExternalAPI()); - -// Wrap with name for better tracing -String namedResult = ctx.run("my-side-effect", String.class, () -> callExternalAPI()); -``` - -### Deterministic randoms and time - -❌ Never use `Math.random()` - non-deterministic and breaks replay logic. -✅ Use `ctx.random()` or `ctx.uuid()` - Restate journals the result for deterministic replay. - -❌ Never use `System.currentTimeMillis()`, `new Date()` - returns different values during replay. -✅ Use `ctx.timer()` - Restate records and replays the same timestamp. - -### Durable Timers and Sleep - -❌ Never use `Thread.sleep()` or `CompletableFuture.delayedExecutor()` - not durable, lost on restarts. -✅ Use `ctx.sleep()` - durable timer that survives failures. - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#durable_timers"} theme={null} -// Sleep -ctx.sleep(Duration.ofSeconds(30)); - -// Schedule delayed call (different from sleep + send) -Target target = Target.service("MyService", "myHandler"); -ctx.send( - Request.of(target, TypeTag.of(String.class), TypeTag.of(String.class), "Hi"), - Duration.ofHours(5)); -``` - -### Awakeables (External Events) - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#awakeables"} theme={null} -// Create awakeable -Awakeable awakeable = ctx.awakeable(String.class); -String awakeableId = awakeable.id(); - -// Send ID to external system -ctx.run(() -> requestHumanReview(name, awakeableId)); - -// Wait for result -String review = awakeable.await(); - -// Resolve from another handler -ctx.awakeableHandle(awakeableId).resolve(String.class, "Looks good!"); - -// Reject from another handler -ctx.awakeableHandle(awakeableId).reject("Cannot be reviewed"); -``` - -### Durable Promises (Workflows only) - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#workflow_promises"} theme={null} -DurablePromiseKey REVIEW_PROMISE = DurablePromiseKey.of("review", String.class); -// Wait for promise -String review = ctx.promise(REVIEW_PROMISE).future().await(); - -// Resolve promise from another handler -ctx.promiseHandle(REVIEW_PROMISE).resolve(review); -``` - -## Concurrency - -Always use Restate combinators (`DurableFuture.all`, `DurableFuture.any`) instead of Java's native `CompletableFuture` methods - they journal execution order for deterministic replay. - -### `DurableFuture.all()` - Wait for All - -Returns when all futures complete. Use to wait for multiple operations to finish. - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#combine_all"} theme={null} -// Wait for all to complete -DurableFuture call1 = MyServiceClient.fromContext(ctx).myHandler("request1"); -DurableFuture call2 = MyServiceClient.fromContext(ctx).myHandler("request2"); - -DurableFuture.all(call1, call2).await(); -``` - -### `DurableFuture.any()` - First Successful Result - -Returns the first successful result, ignoring rejections until all fail. - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#combine_any"} theme={null} -// Wait for any to complete -int indexCompleted = DurableFuture.any(call1, call2).await(); -``` - -### Invocation Management - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#cancel"} theme={null} -var handle = - MyServiceClient.fromContext(ctx) - .send() - .myHandler(request, req -> req.idempotencyKey("abc123")); -var response = handle.attach().await(); -// Cancel invocation -handle.cancel(); -``` - -## Serialization - -### Default (Jackson JSON) - -By default, Java SDK uses Jackson for JSON serialization with POJOs. - -```java Java {"CODE_LOAD::java/src/main/java/develop/SerializationExample.java#state_keys"} theme={null} -// Primitive types -var myString = StateKey.of("myString", String.class); -// Generic types need TypeRef (similar to Jackson's TypeReference) -var myMap = StateKey.of("myMap", TypeTag.of(new TypeRef>() {})); -``` - -### Custom Serialization - -```java {"CODE_LOAD::java/src/main/java/develop/SerializationExample.java#customserdes"} theme={null} -class MyPersonSerde implements Serde { - @Override - public Slice serialize(Person person) { - // convert value to a byte array, then wrap in a Slice - return Slice.wrap(person.toBytes()); - } - - @Override - public Person deserialize(Slice slice) { - // convert value to Person - return Person.fromBytes(slice.toByteArray()); - } -} -``` - -And then use it, for example, in combination with `ctx.run`: - -```java {"CODE_LOAD::java/src/main/java/develop/SerializationExample.java#use_person_serde"} theme={null} -ctx.run(new MyPersonSerde(), () -> new Person()); -``` - -## Error Handling - -Restate retries failures indefinitely by default. For permanent business-logic failures (invalid input, declined payment), use TerminalException to stop retries immediately. - -### Terminal Errors (No Retry) - -```java Java {"CODE_LOAD::java/src/main/java/develop/ErrorHandling.java#here"} theme={null} -throw new TerminalException(500, "Something went wrong"); -``` - -### Retryable Errors - -```java theme={null} -// Any other thrown exception will be retried -throw new RuntimeException("Temporary failure - will retry"); -``` - -## Testing - -```java Java {"CODE_LOAD::java/src/main/java/develop/MyServiceTestMethod.java"} theme={null} -package develop; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import dev.restate.client.Client; -import dev.restate.sdk.testing.BindService; -import dev.restate.sdk.testing.RestateClient; -import dev.restate.sdk.testing.RestateTest; -import org.junit.jupiter.api.Test; - -@RestateTest -class MyServiceTestMethod { - - @BindService MyService service = new MyService(); - - @Test - void testMyHandler(@RestateClient Client ingressClient) { - // Create the service client from the injected ingress client - var client = MyServiceClient.fromClient(ingressClient); - - // Send request to service and assert the response - var response = client.myHandler("Hi"); - assertEquals(response, "Hi!"); - } -} -``` - -## SDK Clients (External Invocations) - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Clients.java#here"} theme={null} -Client restateClient = Client.connect("http://localhost:8080"); - -// Request-response -String result = MyServiceClient.fromClient(restateClient).myHandler("Hi"); - -// One-way -MyServiceClient.fromClient(restateClient).send().myHandler("Hi"); - -// Delayed -MyServiceClient.fromClient(restateClient).send().myHandler("Hi", Duration.ofSeconds(1)); - -// With idempotency key -MyObjectClient.fromClient(restateClient, "Mary") - .send() - .myHandler("Hi", opt -> opt.idempotencyKey("abc")); -``` - - ---- - -> To find navigation and other pages in this documentation, fetch the llms.txt file at: https://docs.restate.dev/llms.txt \ No newline at end of file diff --git a/java/templates/java-maven-quarkus/.claude/CLAUDE.md b/java/templates/java-maven-quarkus/.claude/CLAUDE.md new file mode 120000 index 00000000..be77ac83 --- /dev/null +++ b/java/templates/java-maven-quarkus/.claude/CLAUDE.md @@ -0,0 +1 @@ +../AGENTS.md \ No newline at end of file diff --git a/java/templates/java-maven-quarkus/.cursor/mcp.json b/java/templates/java-maven-quarkus/.cursor/mcp.json deleted file mode 100644 index 78a21c5a..00000000 --- a/java/templates/java-maven-quarkus/.cursor/mcp.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "mcpServers": { - "restate-docs": { - "url": "https://docs.restate.dev/mcp" - } - } -} \ No newline at end of file diff --git a/java/templates/java-maven-quarkus/.cursor/mcp.json b/java/templates/java-maven-quarkus/.cursor/mcp.json new file mode 120000 index 00000000..3aa24b56 --- /dev/null +++ b/java/templates/java-maven-quarkus/.cursor/mcp.json @@ -0,0 +1 @@ +../.ai/mcp/mcp.json \ No newline at end of file diff --git a/java/templates/java-maven-quarkus/.junie/guidelines.md b/java/templates/java-maven-quarkus/.junie/guidelines.md new file mode 120000 index 00000000..be77ac83 --- /dev/null +++ b/java/templates/java-maven-quarkus/.junie/guidelines.md @@ -0,0 +1 @@ +../AGENTS.md \ No newline at end of file diff --git a/java/templates/java-maven-quarkus/.junie/mcp/mcp.json b/java/templates/java-maven-quarkus/.junie/mcp/mcp.json new file mode 120000 index 00000000..f454b32d --- /dev/null +++ b/java/templates/java-maven-quarkus/.junie/mcp/mcp.json @@ -0,0 +1 @@ +../../.ai/mcp/mcp.json \ No newline at end of file diff --git a/java/templates/java-maven-quarkus/.cursor/rules/AGENTS.md b/java/templates/java-maven-quarkus/AGENTS.md similarity index 100% rename from java/templates/java-maven-quarkus/.cursor/rules/AGENTS.md rename to java/templates/java-maven-quarkus/AGENTS.md diff --git a/java/templates/java-maven-spring-boot/.ai/mcp/mcp.json b/java/templates/java-maven-spring-boot/.ai/mcp/mcp.json new file mode 100644 index 00000000..78a21c5a --- /dev/null +++ b/java/templates/java-maven-spring-boot/.ai/mcp/mcp.json @@ -0,0 +1,7 @@ +{ + "mcpServers": { + "restate-docs": { + "url": "https://docs.restate.dev/mcp" + } + } +} \ No newline at end of file diff --git a/java/templates/java-maven-spring-boot/.aiassistant/rules/restate.md b/java/templates/java-maven-spring-boot/.aiassistant/rules/restate.md new file mode 120000 index 00000000..b7e6491d --- /dev/null +++ b/java/templates/java-maven-spring-boot/.aiassistant/rules/restate.md @@ -0,0 +1 @@ +../../AGENTS.md \ No newline at end of file diff --git a/java/templates/java-maven-spring-boot/.claude/CLAUDE.md b/java/templates/java-maven-spring-boot/.claude/CLAUDE.md deleted file mode 100644 index a7454322..00000000 --- a/java/templates/java-maven-spring-boot/.claude/CLAUDE.md +++ /dev/null @@ -1,408 +0,0 @@ -# Restate Java SDK Rules - -## Core Concepts - -* Restate provides durable execution: code automatically stores completed steps and resumes from where it left off on failures -* All handlers receive a `Context`/`ObjectContext`/`WorkflowContext`/`SharedObjectContext`/`SharedWorkflowContext` object as the first argument -* Handlers can take typed inputs and return typed outputs using Java classes and Jackson serialization - -## Service Types - -### Basic Services - -```java Java {"CODE_LOAD::java/src/main/java/develop/MyService.java#here"} theme={null} -import dev.restate.sdk.Context; -import dev.restate.sdk.annotation.Handler; -import dev.restate.sdk.annotation.Service; -import dev.restate.sdk.endpoint.Endpoint; -import dev.restate.sdk.http.vertx.RestateHttpServer; - -@Service -public class MyService { - @Handler - public String myHandler(Context ctx, String greeting) { - return greeting + "!"; - } - - public static void main(String[] args) { - RestateHttpServer.listen(Endpoint.bind(new MyService())); - } -} -``` - -### Virtual Objects (Stateful, Key-Addressable) - -```java Java {"CODE_LOAD::java/src/main/java/develop/MyObject.java#here"} theme={null} -import dev.restate.sdk.ObjectContext; -import dev.restate.sdk.SharedObjectContext; -import dev.restate.sdk.annotation.Handler; -import dev.restate.sdk.annotation.Shared; -import dev.restate.sdk.annotation.VirtualObject; -import dev.restate.sdk.endpoint.Endpoint; -import dev.restate.sdk.http.vertx.RestateHttpServer; - -@VirtualObject -public class MyObject { - - @Handler - public String myHandler(ObjectContext ctx, String greeting) { - String objectId = ctx.key(); - - return greeting + " " + objectId + "!"; - } - - @Shared - public String myConcurrentHandler(SharedObjectContext ctx, String input) { - return "my-output"; - } - - public static void main(String[] args) { - RestateHttpServer.listen(Endpoint.bind(new MyObject())); - } -} -``` - -### Workflows - -```java Java {"CODE_LOAD::java/src/main/java/develop/MyWorkflow.java#here"} theme={null} -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; -import dev.restate.sdk.http.vertx.RestateHttpServer; - -@Workflow -public class MyWorkflow { - - @Workflow - public String run(WorkflowContext ctx, String input) { - - // implement workflow logic here - - return "success"; - } - - @Shared - public String interactWithWorkflow(SharedWorkflowContext ctx, String input) { - // implement interaction logic here - return "my result"; - } - - public static void main(String[] args) { - RestateHttpServer.listen(Endpoint.bind(new MyWorkflow())); - } -} -``` - -## Context Operations - -### State Management (Virtual Objects & Workflows only) - -❌ Never use static variables - not durable, lost across replicas. -✅ Use `ctx.get()` and `ctx.set()` - durable and scoped to the object's key. - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#state"} theme={null} -// Get state keys -Collection keys = ctx.stateKeys(); - -// Get state -StateKey STRING_STATE_KEY = StateKey.of("my-key", String.class); -String stringState = ctx.get(STRING_STATE_KEY).orElse("my-default"); - -StateKey INT_STATE_KEY = StateKey.of("count", Integer.class); -int count = ctx.get(INT_STATE_KEY).orElse(0); - -// Set state -ctx.set(STRING_STATE_KEY, "my-new-value"); -ctx.set(INT_STATE_KEY, count + 1); - -// Clear state -ctx.clear(STRING_STATE_KEY); -ctx.clearAll(); -``` - -### Service Communication - -#### Request-Response - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#service_calls"} theme={null} -// Call a Service -String svcResponse = MyServiceClient.fromContext(ctx).myHandler(request).await(); - -// Call a Virtual Object -String objResponse = MyObjectClient.fromContext(ctx, objectKey).myHandler(request).await(); - -// Call a Workflow -String wfResponse = MyWorkflowClient.fromContext(ctx, workflowId).run(request).await(); -``` - -#### Generic Calls - -Call a service without using the generated client, but just String names. - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#generic_calls"} theme={null} -// Generic service call -Target target = Target.service("MyService", "myHandler"); -String response = - ctx.call(Request.of(target, TypeTag.of(String.class), TypeTag.of(String.class), request)) - .await(); - -// Generic object call -Target objectTarget = Target.virtualObject("MyObject", "object-key", "myHandler"); -String objResponse = - ctx.call( - Request.of( - objectTarget, TypeTag.of(String.class), TypeTag.of(String.class), request)) - .await(); - -// Generic workflow call -Target workflowTarget = Target.workflow("MyWorkflow", "wf-id", "run"); -String wfResponse = - ctx.call( - Request.of( - workflowTarget, TypeTag.of(String.class), TypeTag.of(String.class), request)) - .await(); -``` - -#### One-Way Messages - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#sending_messages"} theme={null} -// Call a Service -MyServiceClient.fromContext(ctx).send().myHandler(request); - -// Call a Virtual Object -MyObjectClient.fromContext(ctx, objectKey).send().myHandler(request); - -// Call a Workflow -MyWorkflowClient.fromContext(ctx, workflowId).send().run(request); -``` - -#### Delayed Messages - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#delayed_messages"} theme={null} -MyServiceClient.fromContext(ctx).send().myHandler(request, Duration.ofDays(5)); -``` - -#### With Idempotency Key - -```java theme={null} -Client restateClient = Client.connect("http://localhost:8080"); -MyServiceClient.fromClient(restateClient) - .send() - .myHandler("Hi", opt -> opt.idempotencyKey("my-key")); -``` - -### Run Actions or Side Effects (Non-Deterministic Operations) - -❌ Never call external APIs/DBs directly - will re-execute during replay, causing duplicates. -✅ Wrap in `ctx.run()` - Restate journals the result; runs only once. - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#durable_steps"} theme={null} -// Wrap non-deterministic code in ctx.run -String result = ctx.run("call external API", String.class, () -> callExternalAPI()); - -// Wrap with name for better tracing -String namedResult = ctx.run("my-side-effect", String.class, () -> callExternalAPI()); -``` - -### Deterministic randoms and time - -❌ Never use `Math.random()` - non-deterministic and breaks replay logic. -✅ Use `ctx.random()` or `ctx.uuid()` - Restate journals the result for deterministic replay. - -❌ Never use `System.currentTimeMillis()`, `new Date()` - returns different values during replay. -✅ Use `ctx.timer()` - Restate records and replays the same timestamp. - -### Durable Timers and Sleep - -❌ Never use `Thread.sleep()` or `CompletableFuture.delayedExecutor()` - not durable, lost on restarts. -✅ Use `ctx.sleep()` - durable timer that survives failures. - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#durable_timers"} theme={null} -// Sleep -ctx.sleep(Duration.ofSeconds(30)); - -// Schedule delayed call (different from sleep + send) -Target target = Target.service("MyService", "myHandler"); -ctx.send( - Request.of(target, TypeTag.of(String.class), TypeTag.of(String.class), "Hi"), - Duration.ofHours(5)); -``` - -### Awakeables (External Events) - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#awakeables"} theme={null} -// Create awakeable -Awakeable awakeable = ctx.awakeable(String.class); -String awakeableId = awakeable.id(); - -// Send ID to external system -ctx.run(() -> requestHumanReview(name, awakeableId)); - -// Wait for result -String review = awakeable.await(); - -// Resolve from another handler -ctx.awakeableHandle(awakeableId).resolve(String.class, "Looks good!"); - -// Reject from another handler -ctx.awakeableHandle(awakeableId).reject("Cannot be reviewed"); -``` - -### Durable Promises (Workflows only) - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#workflow_promises"} theme={null} -DurablePromiseKey REVIEW_PROMISE = DurablePromiseKey.of("review", String.class); -// Wait for promise -String review = ctx.promise(REVIEW_PROMISE).future().await(); - -// Resolve promise from another handler -ctx.promiseHandle(REVIEW_PROMISE).resolve(review); -``` - -## Concurrency - -Always use Restate combinators (`DurableFuture.all`, `DurableFuture.any`) instead of Java's native `CompletableFuture` methods - they journal execution order for deterministic replay. - -### `DurableFuture.all()` - Wait for All - -Returns when all futures complete. Use to wait for multiple operations to finish. - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#combine_all"} theme={null} -// Wait for all to complete -DurableFuture call1 = MyServiceClient.fromContext(ctx).myHandler("request1"); -DurableFuture call2 = MyServiceClient.fromContext(ctx).myHandler("request2"); - -DurableFuture.all(call1, call2).await(); -``` - -### `DurableFuture.any()` - First Successful Result - -Returns the first successful result, ignoring rejections until all fail. - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#combine_any"} theme={null} -// Wait for any to complete -int indexCompleted = DurableFuture.any(call1, call2).await(); -``` - -### Invocation Management - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#cancel"} theme={null} -var handle = - MyServiceClient.fromContext(ctx) - .send() - .myHandler(request, req -> req.idempotencyKey("abc123")); -var response = handle.attach().await(); -// Cancel invocation -handle.cancel(); -``` - -## Serialization - -### Default (Jackson JSON) - -By default, Java SDK uses Jackson for JSON serialization with POJOs. - -```java Java {"CODE_LOAD::java/src/main/java/develop/SerializationExample.java#state_keys"} theme={null} -// Primitive types -var myString = StateKey.of("myString", String.class); -// Generic types need TypeRef (similar to Jackson's TypeReference) -var myMap = StateKey.of("myMap", TypeTag.of(new TypeRef>() {})); -``` - -### Custom Serialization - -```java {"CODE_LOAD::java/src/main/java/develop/SerializationExample.java#customserdes"} theme={null} -class MyPersonSerde implements Serde { - @Override - public Slice serialize(Person person) { - // convert value to a byte array, then wrap in a Slice - return Slice.wrap(person.toBytes()); - } - - @Override - public Person deserialize(Slice slice) { - // convert value to Person - return Person.fromBytes(slice.toByteArray()); - } -} -``` - -And then use it, for example, in combination with `ctx.run`: - -```java {"CODE_LOAD::java/src/main/java/develop/SerializationExample.java#use_person_serde"} theme={null} -ctx.run(new MyPersonSerde(), () -> new Person()); -``` - -## Error Handling - -Restate retries failures indefinitely by default. For permanent business-logic failures (invalid input, declined payment), use TerminalException to stop retries immediately. - -### Terminal Errors (No Retry) - -```java Java {"CODE_LOAD::java/src/main/java/develop/ErrorHandling.java#here"} theme={null} -throw new TerminalException(500, "Something went wrong"); -``` - -### Retryable Errors - -```java theme={null} -// Any other thrown exception will be retried -throw new RuntimeException("Temporary failure - will retry"); -``` - -## Testing - -```java Java {"CODE_LOAD::java/src/main/java/develop/MyServiceTestMethod.java"} theme={null} -package develop; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import dev.restate.client.Client; -import dev.restate.sdk.testing.BindService; -import dev.restate.sdk.testing.RestateClient; -import dev.restate.sdk.testing.RestateTest; -import org.junit.jupiter.api.Test; - -@RestateTest -class MyServiceTestMethod { - - @BindService MyService service = new MyService(); - - @Test - void testMyHandler(@RestateClient Client ingressClient) { - // Create the service client from the injected ingress client - var client = MyServiceClient.fromClient(ingressClient); - - // Send request to service and assert the response - var response = client.myHandler("Hi"); - assertEquals(response, "Hi!"); - } -} -``` - -## SDK Clients (External Invocations) - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Clients.java#here"} theme={null} -Client restateClient = Client.connect("http://localhost:8080"); - -// Request-response -String result = MyServiceClient.fromClient(restateClient).myHandler("Hi"); - -// One-way -MyServiceClient.fromClient(restateClient).send().myHandler("Hi"); - -// Delayed -MyServiceClient.fromClient(restateClient).send().myHandler("Hi", Duration.ofSeconds(1)); - -// With idempotency key -MyObjectClient.fromClient(restateClient, "Mary") - .send() - .myHandler("Hi", opt -> opt.idempotencyKey("abc")); -``` - - ---- - -> To find navigation and other pages in this documentation, fetch the llms.txt file at: https://docs.restate.dev/llms.txt \ No newline at end of file diff --git a/java/templates/java-maven-spring-boot/.claude/CLAUDE.md b/java/templates/java-maven-spring-boot/.claude/CLAUDE.md new file mode 120000 index 00000000..be77ac83 --- /dev/null +++ b/java/templates/java-maven-spring-boot/.claude/CLAUDE.md @@ -0,0 +1 @@ +../AGENTS.md \ No newline at end of file diff --git a/java/templates/java-maven-spring-boot/.cursor/mcp.json b/java/templates/java-maven-spring-boot/.cursor/mcp.json deleted file mode 100644 index 78a21c5a..00000000 --- a/java/templates/java-maven-spring-boot/.cursor/mcp.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "mcpServers": { - "restate-docs": { - "url": "https://docs.restate.dev/mcp" - } - } -} \ No newline at end of file diff --git a/java/templates/java-maven-spring-boot/.cursor/mcp.json b/java/templates/java-maven-spring-boot/.cursor/mcp.json new file mode 120000 index 00000000..3aa24b56 --- /dev/null +++ b/java/templates/java-maven-spring-boot/.cursor/mcp.json @@ -0,0 +1 @@ +../.ai/mcp/mcp.json \ No newline at end of file diff --git a/java/templates/java-maven-spring-boot/.junie/guidelines.md b/java/templates/java-maven-spring-boot/.junie/guidelines.md new file mode 120000 index 00000000..be77ac83 --- /dev/null +++ b/java/templates/java-maven-spring-boot/.junie/guidelines.md @@ -0,0 +1 @@ +../AGENTS.md \ No newline at end of file diff --git a/java/templates/java-maven-spring-boot/.junie/mcp/mcp.json b/java/templates/java-maven-spring-boot/.junie/mcp/mcp.json new file mode 120000 index 00000000..f454b32d --- /dev/null +++ b/java/templates/java-maven-spring-boot/.junie/mcp/mcp.json @@ -0,0 +1 @@ +../../.ai/mcp/mcp.json \ No newline at end of file diff --git a/java/templates/java-maven-spring-boot/.cursor/rules/AGENTS.md b/java/templates/java-maven-spring-boot/AGENTS.md similarity index 100% rename from java/templates/java-maven-spring-boot/.cursor/rules/AGENTS.md rename to java/templates/java-maven-spring-boot/AGENTS.md diff --git a/java/templates/java-maven/.ai/mcp/mcp.json b/java/templates/java-maven/.ai/mcp/mcp.json new file mode 100644 index 00000000..78a21c5a --- /dev/null +++ b/java/templates/java-maven/.ai/mcp/mcp.json @@ -0,0 +1,7 @@ +{ + "mcpServers": { + "restate-docs": { + "url": "https://docs.restate.dev/mcp" + } + } +} \ No newline at end of file diff --git a/java/templates/java-maven/.aiassistant/rules/restate.md b/java/templates/java-maven/.aiassistant/rules/restate.md new file mode 120000 index 00000000..b7e6491d --- /dev/null +++ b/java/templates/java-maven/.aiassistant/rules/restate.md @@ -0,0 +1 @@ +../../AGENTS.md \ No newline at end of file diff --git a/java/templates/java-maven/.claude/CLAUDE.md b/java/templates/java-maven/.claude/CLAUDE.md deleted file mode 100644 index a7454322..00000000 --- a/java/templates/java-maven/.claude/CLAUDE.md +++ /dev/null @@ -1,408 +0,0 @@ -# Restate Java SDK Rules - -## Core Concepts - -* Restate provides durable execution: code automatically stores completed steps and resumes from where it left off on failures -* All handlers receive a `Context`/`ObjectContext`/`WorkflowContext`/`SharedObjectContext`/`SharedWorkflowContext` object as the first argument -* Handlers can take typed inputs and return typed outputs using Java classes and Jackson serialization - -## Service Types - -### Basic Services - -```java Java {"CODE_LOAD::java/src/main/java/develop/MyService.java#here"} theme={null} -import dev.restate.sdk.Context; -import dev.restate.sdk.annotation.Handler; -import dev.restate.sdk.annotation.Service; -import dev.restate.sdk.endpoint.Endpoint; -import dev.restate.sdk.http.vertx.RestateHttpServer; - -@Service -public class MyService { - @Handler - public String myHandler(Context ctx, String greeting) { - return greeting + "!"; - } - - public static void main(String[] args) { - RestateHttpServer.listen(Endpoint.bind(new MyService())); - } -} -``` - -### Virtual Objects (Stateful, Key-Addressable) - -```java Java {"CODE_LOAD::java/src/main/java/develop/MyObject.java#here"} theme={null} -import dev.restate.sdk.ObjectContext; -import dev.restate.sdk.SharedObjectContext; -import dev.restate.sdk.annotation.Handler; -import dev.restate.sdk.annotation.Shared; -import dev.restate.sdk.annotation.VirtualObject; -import dev.restate.sdk.endpoint.Endpoint; -import dev.restate.sdk.http.vertx.RestateHttpServer; - -@VirtualObject -public class MyObject { - - @Handler - public String myHandler(ObjectContext ctx, String greeting) { - String objectId = ctx.key(); - - return greeting + " " + objectId + "!"; - } - - @Shared - public String myConcurrentHandler(SharedObjectContext ctx, String input) { - return "my-output"; - } - - public static void main(String[] args) { - RestateHttpServer.listen(Endpoint.bind(new MyObject())); - } -} -``` - -### Workflows - -```java Java {"CODE_LOAD::java/src/main/java/develop/MyWorkflow.java#here"} theme={null} -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; -import dev.restate.sdk.http.vertx.RestateHttpServer; - -@Workflow -public class MyWorkflow { - - @Workflow - public String run(WorkflowContext ctx, String input) { - - // implement workflow logic here - - return "success"; - } - - @Shared - public String interactWithWorkflow(SharedWorkflowContext ctx, String input) { - // implement interaction logic here - return "my result"; - } - - public static void main(String[] args) { - RestateHttpServer.listen(Endpoint.bind(new MyWorkflow())); - } -} -``` - -## Context Operations - -### State Management (Virtual Objects & Workflows only) - -❌ Never use static variables - not durable, lost across replicas. -✅ Use `ctx.get()` and `ctx.set()` - durable and scoped to the object's key. - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#state"} theme={null} -// Get state keys -Collection keys = ctx.stateKeys(); - -// Get state -StateKey STRING_STATE_KEY = StateKey.of("my-key", String.class); -String stringState = ctx.get(STRING_STATE_KEY).orElse("my-default"); - -StateKey INT_STATE_KEY = StateKey.of("count", Integer.class); -int count = ctx.get(INT_STATE_KEY).orElse(0); - -// Set state -ctx.set(STRING_STATE_KEY, "my-new-value"); -ctx.set(INT_STATE_KEY, count + 1); - -// Clear state -ctx.clear(STRING_STATE_KEY); -ctx.clearAll(); -``` - -### Service Communication - -#### Request-Response - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#service_calls"} theme={null} -// Call a Service -String svcResponse = MyServiceClient.fromContext(ctx).myHandler(request).await(); - -// Call a Virtual Object -String objResponse = MyObjectClient.fromContext(ctx, objectKey).myHandler(request).await(); - -// Call a Workflow -String wfResponse = MyWorkflowClient.fromContext(ctx, workflowId).run(request).await(); -``` - -#### Generic Calls - -Call a service without using the generated client, but just String names. - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#generic_calls"} theme={null} -// Generic service call -Target target = Target.service("MyService", "myHandler"); -String response = - ctx.call(Request.of(target, TypeTag.of(String.class), TypeTag.of(String.class), request)) - .await(); - -// Generic object call -Target objectTarget = Target.virtualObject("MyObject", "object-key", "myHandler"); -String objResponse = - ctx.call( - Request.of( - objectTarget, TypeTag.of(String.class), TypeTag.of(String.class), request)) - .await(); - -// Generic workflow call -Target workflowTarget = Target.workflow("MyWorkflow", "wf-id", "run"); -String wfResponse = - ctx.call( - Request.of( - workflowTarget, TypeTag.of(String.class), TypeTag.of(String.class), request)) - .await(); -``` - -#### One-Way Messages - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#sending_messages"} theme={null} -// Call a Service -MyServiceClient.fromContext(ctx).send().myHandler(request); - -// Call a Virtual Object -MyObjectClient.fromContext(ctx, objectKey).send().myHandler(request); - -// Call a Workflow -MyWorkflowClient.fromContext(ctx, workflowId).send().run(request); -``` - -#### Delayed Messages - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#delayed_messages"} theme={null} -MyServiceClient.fromContext(ctx).send().myHandler(request, Duration.ofDays(5)); -``` - -#### With Idempotency Key - -```java theme={null} -Client restateClient = Client.connect("http://localhost:8080"); -MyServiceClient.fromClient(restateClient) - .send() - .myHandler("Hi", opt -> opt.idempotencyKey("my-key")); -``` - -### Run Actions or Side Effects (Non-Deterministic Operations) - -❌ Never call external APIs/DBs directly - will re-execute during replay, causing duplicates. -✅ Wrap in `ctx.run()` - Restate journals the result; runs only once. - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#durable_steps"} theme={null} -// Wrap non-deterministic code in ctx.run -String result = ctx.run("call external API", String.class, () -> callExternalAPI()); - -// Wrap with name for better tracing -String namedResult = ctx.run("my-side-effect", String.class, () -> callExternalAPI()); -``` - -### Deterministic randoms and time - -❌ Never use `Math.random()` - non-deterministic and breaks replay logic. -✅ Use `ctx.random()` or `ctx.uuid()` - Restate journals the result for deterministic replay. - -❌ Never use `System.currentTimeMillis()`, `new Date()` - returns different values during replay. -✅ Use `ctx.timer()` - Restate records and replays the same timestamp. - -### Durable Timers and Sleep - -❌ Never use `Thread.sleep()` or `CompletableFuture.delayedExecutor()` - not durable, lost on restarts. -✅ Use `ctx.sleep()` - durable timer that survives failures. - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#durable_timers"} theme={null} -// Sleep -ctx.sleep(Duration.ofSeconds(30)); - -// Schedule delayed call (different from sleep + send) -Target target = Target.service("MyService", "myHandler"); -ctx.send( - Request.of(target, TypeTag.of(String.class), TypeTag.of(String.class), "Hi"), - Duration.ofHours(5)); -``` - -### Awakeables (External Events) - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#awakeables"} theme={null} -// Create awakeable -Awakeable awakeable = ctx.awakeable(String.class); -String awakeableId = awakeable.id(); - -// Send ID to external system -ctx.run(() -> requestHumanReview(name, awakeableId)); - -// Wait for result -String review = awakeable.await(); - -// Resolve from another handler -ctx.awakeableHandle(awakeableId).resolve(String.class, "Looks good!"); - -// Reject from another handler -ctx.awakeableHandle(awakeableId).reject("Cannot be reviewed"); -``` - -### Durable Promises (Workflows only) - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#workflow_promises"} theme={null} -DurablePromiseKey REVIEW_PROMISE = DurablePromiseKey.of("review", String.class); -// Wait for promise -String review = ctx.promise(REVIEW_PROMISE).future().await(); - -// Resolve promise from another handler -ctx.promiseHandle(REVIEW_PROMISE).resolve(review); -``` - -## Concurrency - -Always use Restate combinators (`DurableFuture.all`, `DurableFuture.any`) instead of Java's native `CompletableFuture` methods - they journal execution order for deterministic replay. - -### `DurableFuture.all()` - Wait for All - -Returns when all futures complete. Use to wait for multiple operations to finish. - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#combine_all"} theme={null} -// Wait for all to complete -DurableFuture call1 = MyServiceClient.fromContext(ctx).myHandler("request1"); -DurableFuture call2 = MyServiceClient.fromContext(ctx).myHandler("request2"); - -DurableFuture.all(call1, call2).await(); -``` - -### `DurableFuture.any()` - First Successful Result - -Returns the first successful result, ignoring rejections until all fail. - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#combine_any"} theme={null} -// Wait for any to complete -int indexCompleted = DurableFuture.any(call1, call2).await(); -``` - -### Invocation Management - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#cancel"} theme={null} -var handle = - MyServiceClient.fromContext(ctx) - .send() - .myHandler(request, req -> req.idempotencyKey("abc123")); -var response = handle.attach().await(); -// Cancel invocation -handle.cancel(); -``` - -## Serialization - -### Default (Jackson JSON) - -By default, Java SDK uses Jackson for JSON serialization with POJOs. - -```java Java {"CODE_LOAD::java/src/main/java/develop/SerializationExample.java#state_keys"} theme={null} -// Primitive types -var myString = StateKey.of("myString", String.class); -// Generic types need TypeRef (similar to Jackson's TypeReference) -var myMap = StateKey.of("myMap", TypeTag.of(new TypeRef>() {})); -``` - -### Custom Serialization - -```java {"CODE_LOAD::java/src/main/java/develop/SerializationExample.java#customserdes"} theme={null} -class MyPersonSerde implements Serde { - @Override - public Slice serialize(Person person) { - // convert value to a byte array, then wrap in a Slice - return Slice.wrap(person.toBytes()); - } - - @Override - public Person deserialize(Slice slice) { - // convert value to Person - return Person.fromBytes(slice.toByteArray()); - } -} -``` - -And then use it, for example, in combination with `ctx.run`: - -```java {"CODE_LOAD::java/src/main/java/develop/SerializationExample.java#use_person_serde"} theme={null} -ctx.run(new MyPersonSerde(), () -> new Person()); -``` - -## Error Handling - -Restate retries failures indefinitely by default. For permanent business-logic failures (invalid input, declined payment), use TerminalException to stop retries immediately. - -### Terminal Errors (No Retry) - -```java Java {"CODE_LOAD::java/src/main/java/develop/ErrorHandling.java#here"} theme={null} -throw new TerminalException(500, "Something went wrong"); -``` - -### Retryable Errors - -```java theme={null} -// Any other thrown exception will be retried -throw new RuntimeException("Temporary failure - will retry"); -``` - -## Testing - -```java Java {"CODE_LOAD::java/src/main/java/develop/MyServiceTestMethod.java"} theme={null} -package develop; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import dev.restate.client.Client; -import dev.restate.sdk.testing.BindService; -import dev.restate.sdk.testing.RestateClient; -import dev.restate.sdk.testing.RestateTest; -import org.junit.jupiter.api.Test; - -@RestateTest -class MyServiceTestMethod { - - @BindService MyService service = new MyService(); - - @Test - void testMyHandler(@RestateClient Client ingressClient) { - // Create the service client from the injected ingress client - var client = MyServiceClient.fromClient(ingressClient); - - // Send request to service and assert the response - var response = client.myHandler("Hi"); - assertEquals(response, "Hi!"); - } -} -``` - -## SDK Clients (External Invocations) - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Clients.java#here"} theme={null} -Client restateClient = Client.connect("http://localhost:8080"); - -// Request-response -String result = MyServiceClient.fromClient(restateClient).myHandler("Hi"); - -// One-way -MyServiceClient.fromClient(restateClient).send().myHandler("Hi"); - -// Delayed -MyServiceClient.fromClient(restateClient).send().myHandler("Hi", Duration.ofSeconds(1)); - -// With idempotency key -MyObjectClient.fromClient(restateClient, "Mary") - .send() - .myHandler("Hi", opt -> opt.idempotencyKey("abc")); -``` - - ---- - -> To find navigation and other pages in this documentation, fetch the llms.txt file at: https://docs.restate.dev/llms.txt \ No newline at end of file diff --git a/java/templates/java-maven/.claude/CLAUDE.md b/java/templates/java-maven/.claude/CLAUDE.md new file mode 120000 index 00000000..be77ac83 --- /dev/null +++ b/java/templates/java-maven/.claude/CLAUDE.md @@ -0,0 +1 @@ +../AGENTS.md \ No newline at end of file diff --git a/java/templates/java-maven/.cursor/mcp.json b/java/templates/java-maven/.cursor/mcp.json deleted file mode 100644 index 78a21c5a..00000000 --- a/java/templates/java-maven/.cursor/mcp.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "mcpServers": { - "restate-docs": { - "url": "https://docs.restate.dev/mcp" - } - } -} \ No newline at end of file diff --git a/java/templates/java-maven/.cursor/mcp.json b/java/templates/java-maven/.cursor/mcp.json new file mode 120000 index 00000000..3aa24b56 --- /dev/null +++ b/java/templates/java-maven/.cursor/mcp.json @@ -0,0 +1 @@ +../.ai/mcp/mcp.json \ No newline at end of file diff --git a/java/templates/java-maven/.junie/guidelines.md b/java/templates/java-maven/.junie/guidelines.md new file mode 120000 index 00000000..be77ac83 --- /dev/null +++ b/java/templates/java-maven/.junie/guidelines.md @@ -0,0 +1 @@ +../AGENTS.md \ No newline at end of file diff --git a/java/templates/java-maven/.junie/mcp/mcp.json b/java/templates/java-maven/.junie/mcp/mcp.json new file mode 120000 index 00000000..f454b32d --- /dev/null +++ b/java/templates/java-maven/.junie/mcp/mcp.json @@ -0,0 +1 @@ +../../.ai/mcp/mcp.json \ No newline at end of file diff --git a/java/templates/java-maven/.cursor/rules/AGENTS.md b/java/templates/java-maven/AGENTS.md similarity index 100% rename from java/templates/java-maven/.cursor/rules/AGENTS.md rename to java/templates/java-maven/AGENTS.md diff --git a/java/templates/java-new-api-gradle/.ai/mcp/mcp.json b/java/templates/java-new-api-gradle/.ai/mcp/mcp.json new file mode 100644 index 00000000..78a21c5a --- /dev/null +++ b/java/templates/java-new-api-gradle/.ai/mcp/mcp.json @@ -0,0 +1,7 @@ +{ + "mcpServers": { + "restate-docs": { + "url": "https://docs.restate.dev/mcp" + } + } +} \ No newline at end of file diff --git a/java/templates/java-new-api-gradle/.aiassistant/rules/restate.md b/java/templates/java-new-api-gradle/.aiassistant/rules/restate.md new file mode 120000 index 00000000..b7e6491d --- /dev/null +++ b/java/templates/java-new-api-gradle/.aiassistant/rules/restate.md @@ -0,0 +1 @@ +../../AGENTS.md \ No newline at end of file diff --git a/java/templates/java-new-api-gradle/.claude/CLAUDE.md b/java/templates/java-new-api-gradle/.claude/CLAUDE.md deleted file mode 100644 index a7454322..00000000 --- a/java/templates/java-new-api-gradle/.claude/CLAUDE.md +++ /dev/null @@ -1,408 +0,0 @@ -# Restate Java SDK Rules - -## Core Concepts - -* Restate provides durable execution: code automatically stores completed steps and resumes from where it left off on failures -* All handlers receive a `Context`/`ObjectContext`/`WorkflowContext`/`SharedObjectContext`/`SharedWorkflowContext` object as the first argument -* Handlers can take typed inputs and return typed outputs using Java classes and Jackson serialization - -## Service Types - -### Basic Services - -```java Java {"CODE_LOAD::java/src/main/java/develop/MyService.java#here"} theme={null} -import dev.restate.sdk.Context; -import dev.restate.sdk.annotation.Handler; -import dev.restate.sdk.annotation.Service; -import dev.restate.sdk.endpoint.Endpoint; -import dev.restate.sdk.http.vertx.RestateHttpServer; - -@Service -public class MyService { - @Handler - public String myHandler(Context ctx, String greeting) { - return greeting + "!"; - } - - public static void main(String[] args) { - RestateHttpServer.listen(Endpoint.bind(new MyService())); - } -} -``` - -### Virtual Objects (Stateful, Key-Addressable) - -```java Java {"CODE_LOAD::java/src/main/java/develop/MyObject.java#here"} theme={null} -import dev.restate.sdk.ObjectContext; -import dev.restate.sdk.SharedObjectContext; -import dev.restate.sdk.annotation.Handler; -import dev.restate.sdk.annotation.Shared; -import dev.restate.sdk.annotation.VirtualObject; -import dev.restate.sdk.endpoint.Endpoint; -import dev.restate.sdk.http.vertx.RestateHttpServer; - -@VirtualObject -public class MyObject { - - @Handler - public String myHandler(ObjectContext ctx, String greeting) { - String objectId = ctx.key(); - - return greeting + " " + objectId + "!"; - } - - @Shared - public String myConcurrentHandler(SharedObjectContext ctx, String input) { - return "my-output"; - } - - public static void main(String[] args) { - RestateHttpServer.listen(Endpoint.bind(new MyObject())); - } -} -``` - -### Workflows - -```java Java {"CODE_LOAD::java/src/main/java/develop/MyWorkflow.java#here"} theme={null} -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; -import dev.restate.sdk.http.vertx.RestateHttpServer; - -@Workflow -public class MyWorkflow { - - @Workflow - public String run(WorkflowContext ctx, String input) { - - // implement workflow logic here - - return "success"; - } - - @Shared - public String interactWithWorkflow(SharedWorkflowContext ctx, String input) { - // implement interaction logic here - return "my result"; - } - - public static void main(String[] args) { - RestateHttpServer.listen(Endpoint.bind(new MyWorkflow())); - } -} -``` - -## Context Operations - -### State Management (Virtual Objects & Workflows only) - -❌ Never use static variables - not durable, lost across replicas. -✅ Use `ctx.get()` and `ctx.set()` - durable and scoped to the object's key. - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#state"} theme={null} -// Get state keys -Collection keys = ctx.stateKeys(); - -// Get state -StateKey STRING_STATE_KEY = StateKey.of("my-key", String.class); -String stringState = ctx.get(STRING_STATE_KEY).orElse("my-default"); - -StateKey INT_STATE_KEY = StateKey.of("count", Integer.class); -int count = ctx.get(INT_STATE_KEY).orElse(0); - -// Set state -ctx.set(STRING_STATE_KEY, "my-new-value"); -ctx.set(INT_STATE_KEY, count + 1); - -// Clear state -ctx.clear(STRING_STATE_KEY); -ctx.clearAll(); -``` - -### Service Communication - -#### Request-Response - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#service_calls"} theme={null} -// Call a Service -String svcResponse = MyServiceClient.fromContext(ctx).myHandler(request).await(); - -// Call a Virtual Object -String objResponse = MyObjectClient.fromContext(ctx, objectKey).myHandler(request).await(); - -// Call a Workflow -String wfResponse = MyWorkflowClient.fromContext(ctx, workflowId).run(request).await(); -``` - -#### Generic Calls - -Call a service without using the generated client, but just String names. - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#generic_calls"} theme={null} -// Generic service call -Target target = Target.service("MyService", "myHandler"); -String response = - ctx.call(Request.of(target, TypeTag.of(String.class), TypeTag.of(String.class), request)) - .await(); - -// Generic object call -Target objectTarget = Target.virtualObject("MyObject", "object-key", "myHandler"); -String objResponse = - ctx.call( - Request.of( - objectTarget, TypeTag.of(String.class), TypeTag.of(String.class), request)) - .await(); - -// Generic workflow call -Target workflowTarget = Target.workflow("MyWorkflow", "wf-id", "run"); -String wfResponse = - ctx.call( - Request.of( - workflowTarget, TypeTag.of(String.class), TypeTag.of(String.class), request)) - .await(); -``` - -#### One-Way Messages - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#sending_messages"} theme={null} -// Call a Service -MyServiceClient.fromContext(ctx).send().myHandler(request); - -// Call a Virtual Object -MyObjectClient.fromContext(ctx, objectKey).send().myHandler(request); - -// Call a Workflow -MyWorkflowClient.fromContext(ctx, workflowId).send().run(request); -``` - -#### Delayed Messages - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#delayed_messages"} theme={null} -MyServiceClient.fromContext(ctx).send().myHandler(request, Duration.ofDays(5)); -``` - -#### With Idempotency Key - -```java theme={null} -Client restateClient = Client.connect("http://localhost:8080"); -MyServiceClient.fromClient(restateClient) - .send() - .myHandler("Hi", opt -> opt.idempotencyKey("my-key")); -``` - -### Run Actions or Side Effects (Non-Deterministic Operations) - -❌ Never call external APIs/DBs directly - will re-execute during replay, causing duplicates. -✅ Wrap in `ctx.run()` - Restate journals the result; runs only once. - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#durable_steps"} theme={null} -// Wrap non-deterministic code in ctx.run -String result = ctx.run("call external API", String.class, () -> callExternalAPI()); - -// Wrap with name for better tracing -String namedResult = ctx.run("my-side-effect", String.class, () -> callExternalAPI()); -``` - -### Deterministic randoms and time - -❌ Never use `Math.random()` - non-deterministic and breaks replay logic. -✅ Use `ctx.random()` or `ctx.uuid()` - Restate journals the result for deterministic replay. - -❌ Never use `System.currentTimeMillis()`, `new Date()` - returns different values during replay. -✅ Use `ctx.timer()` - Restate records and replays the same timestamp. - -### Durable Timers and Sleep - -❌ Never use `Thread.sleep()` or `CompletableFuture.delayedExecutor()` - not durable, lost on restarts. -✅ Use `ctx.sleep()` - durable timer that survives failures. - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#durable_timers"} theme={null} -// Sleep -ctx.sleep(Duration.ofSeconds(30)); - -// Schedule delayed call (different from sleep + send) -Target target = Target.service("MyService", "myHandler"); -ctx.send( - Request.of(target, TypeTag.of(String.class), TypeTag.of(String.class), "Hi"), - Duration.ofHours(5)); -``` - -### Awakeables (External Events) - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#awakeables"} theme={null} -// Create awakeable -Awakeable awakeable = ctx.awakeable(String.class); -String awakeableId = awakeable.id(); - -// Send ID to external system -ctx.run(() -> requestHumanReview(name, awakeableId)); - -// Wait for result -String review = awakeable.await(); - -// Resolve from another handler -ctx.awakeableHandle(awakeableId).resolve(String.class, "Looks good!"); - -// Reject from another handler -ctx.awakeableHandle(awakeableId).reject("Cannot be reviewed"); -``` - -### Durable Promises (Workflows only) - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#workflow_promises"} theme={null} -DurablePromiseKey REVIEW_PROMISE = DurablePromiseKey.of("review", String.class); -// Wait for promise -String review = ctx.promise(REVIEW_PROMISE).future().await(); - -// Resolve promise from another handler -ctx.promiseHandle(REVIEW_PROMISE).resolve(review); -``` - -## Concurrency - -Always use Restate combinators (`DurableFuture.all`, `DurableFuture.any`) instead of Java's native `CompletableFuture` methods - they journal execution order for deterministic replay. - -### `DurableFuture.all()` - Wait for All - -Returns when all futures complete. Use to wait for multiple operations to finish. - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#combine_all"} theme={null} -// Wait for all to complete -DurableFuture call1 = MyServiceClient.fromContext(ctx).myHandler("request1"); -DurableFuture call2 = MyServiceClient.fromContext(ctx).myHandler("request2"); - -DurableFuture.all(call1, call2).await(); -``` - -### `DurableFuture.any()` - First Successful Result - -Returns the first successful result, ignoring rejections until all fail. - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#combine_any"} theme={null} -// Wait for any to complete -int indexCompleted = DurableFuture.any(call1, call2).await(); -``` - -### Invocation Management - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#cancel"} theme={null} -var handle = - MyServiceClient.fromContext(ctx) - .send() - .myHandler(request, req -> req.idempotencyKey("abc123")); -var response = handle.attach().await(); -// Cancel invocation -handle.cancel(); -``` - -## Serialization - -### Default (Jackson JSON) - -By default, Java SDK uses Jackson for JSON serialization with POJOs. - -```java Java {"CODE_LOAD::java/src/main/java/develop/SerializationExample.java#state_keys"} theme={null} -// Primitive types -var myString = StateKey.of("myString", String.class); -// Generic types need TypeRef (similar to Jackson's TypeReference) -var myMap = StateKey.of("myMap", TypeTag.of(new TypeRef>() {})); -``` - -### Custom Serialization - -```java {"CODE_LOAD::java/src/main/java/develop/SerializationExample.java#customserdes"} theme={null} -class MyPersonSerde implements Serde { - @Override - public Slice serialize(Person person) { - // convert value to a byte array, then wrap in a Slice - return Slice.wrap(person.toBytes()); - } - - @Override - public Person deserialize(Slice slice) { - // convert value to Person - return Person.fromBytes(slice.toByteArray()); - } -} -``` - -And then use it, for example, in combination with `ctx.run`: - -```java {"CODE_LOAD::java/src/main/java/develop/SerializationExample.java#use_person_serde"} theme={null} -ctx.run(new MyPersonSerde(), () -> new Person()); -``` - -## Error Handling - -Restate retries failures indefinitely by default. For permanent business-logic failures (invalid input, declined payment), use TerminalException to stop retries immediately. - -### Terminal Errors (No Retry) - -```java Java {"CODE_LOAD::java/src/main/java/develop/ErrorHandling.java#here"} theme={null} -throw new TerminalException(500, "Something went wrong"); -``` - -### Retryable Errors - -```java theme={null} -// Any other thrown exception will be retried -throw new RuntimeException("Temporary failure - will retry"); -``` - -## Testing - -```java Java {"CODE_LOAD::java/src/main/java/develop/MyServiceTestMethod.java"} theme={null} -package develop; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import dev.restate.client.Client; -import dev.restate.sdk.testing.BindService; -import dev.restate.sdk.testing.RestateClient; -import dev.restate.sdk.testing.RestateTest; -import org.junit.jupiter.api.Test; - -@RestateTest -class MyServiceTestMethod { - - @BindService MyService service = new MyService(); - - @Test - void testMyHandler(@RestateClient Client ingressClient) { - // Create the service client from the injected ingress client - var client = MyServiceClient.fromClient(ingressClient); - - // Send request to service and assert the response - var response = client.myHandler("Hi"); - assertEquals(response, "Hi!"); - } -} -``` - -## SDK Clients (External Invocations) - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Clients.java#here"} theme={null} -Client restateClient = Client.connect("http://localhost:8080"); - -// Request-response -String result = MyServiceClient.fromClient(restateClient).myHandler("Hi"); - -// One-way -MyServiceClient.fromClient(restateClient).send().myHandler("Hi"); - -// Delayed -MyServiceClient.fromClient(restateClient).send().myHandler("Hi", Duration.ofSeconds(1)); - -// With idempotency key -MyObjectClient.fromClient(restateClient, "Mary") - .send() - .myHandler("Hi", opt -> opt.idempotencyKey("abc")); -``` - - ---- - -> To find navigation and other pages in this documentation, fetch the llms.txt file at: https://docs.restate.dev/llms.txt \ No newline at end of file diff --git a/java/templates/java-new-api-gradle/.claude/CLAUDE.md b/java/templates/java-new-api-gradle/.claude/CLAUDE.md new file mode 120000 index 00000000..be77ac83 --- /dev/null +++ b/java/templates/java-new-api-gradle/.claude/CLAUDE.md @@ -0,0 +1 @@ +../AGENTS.md \ No newline at end of file diff --git a/java/templates/java-new-api-gradle/.cursor/mcp.json b/java/templates/java-new-api-gradle/.cursor/mcp.json deleted file mode 100644 index 78a21c5a..00000000 --- a/java/templates/java-new-api-gradle/.cursor/mcp.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "mcpServers": { - "restate-docs": { - "url": "https://docs.restate.dev/mcp" - } - } -} \ No newline at end of file diff --git a/java/templates/java-new-api-gradle/.cursor/mcp.json b/java/templates/java-new-api-gradle/.cursor/mcp.json new file mode 120000 index 00000000..3aa24b56 --- /dev/null +++ b/java/templates/java-new-api-gradle/.cursor/mcp.json @@ -0,0 +1 @@ +../.ai/mcp/mcp.json \ No newline at end of file diff --git a/java/templates/java-new-api-gradle/.junie/guidelines.md b/java/templates/java-new-api-gradle/.junie/guidelines.md new file mode 120000 index 00000000..be77ac83 --- /dev/null +++ b/java/templates/java-new-api-gradle/.junie/guidelines.md @@ -0,0 +1 @@ +../AGENTS.md \ No newline at end of file diff --git a/java/templates/java-new-api-gradle/.junie/mcp/mcp.json b/java/templates/java-new-api-gradle/.junie/mcp/mcp.json new file mode 120000 index 00000000..f454b32d --- /dev/null +++ b/java/templates/java-new-api-gradle/.junie/mcp/mcp.json @@ -0,0 +1 @@ +../../.ai/mcp/mcp.json \ No newline at end of file diff --git a/java/templates/java-new-api-gradle/.cursor/rules/AGENTS.md b/java/templates/java-new-api-gradle/AGENTS.md similarity index 100% rename from java/templates/java-new-api-gradle/.cursor/rules/AGENTS.md rename to java/templates/java-new-api-gradle/AGENTS.md diff --git a/java/templates/java-new-api-gradle/README.md b/java/templates/java-new-api-gradle/README.md index 25d85dea..c2e1bbdc 100644 --- a/java/templates/java-new-api-gradle/README.md +++ b/java/templates/java-new-api-gradle/README.md @@ -8,7 +8,4 @@ To run: ```shell ./gradlew run -``` - -Restate SDK uses annotation processing to generate client classes. -When modifying the annotated services in Intellij, it is suggested to run **CTRL + F9** to re-generate the client classes. \ No newline at end of file +``` \ No newline at end of file diff --git a/java/templates/java-new-api-maven-spring-boot/.ai/mcp/mcp.json b/java/templates/java-new-api-maven-spring-boot/.ai/mcp/mcp.json new file mode 100644 index 00000000..78a21c5a --- /dev/null +++ b/java/templates/java-new-api-maven-spring-boot/.ai/mcp/mcp.json @@ -0,0 +1,7 @@ +{ + "mcpServers": { + "restate-docs": { + "url": "https://docs.restate.dev/mcp" + } + } +} \ No newline at end of file diff --git a/java/templates/java-new-api-maven-spring-boot/.aiassistant/rules/restate.md b/java/templates/java-new-api-maven-spring-boot/.aiassistant/rules/restate.md new file mode 120000 index 00000000..b7e6491d --- /dev/null +++ b/java/templates/java-new-api-maven-spring-boot/.aiassistant/rules/restate.md @@ -0,0 +1 @@ +../../AGENTS.md \ No newline at end of file diff --git a/java/templates/java-new-api-maven-spring-boot/.claude/CLAUDE.md b/java/templates/java-new-api-maven-spring-boot/.claude/CLAUDE.md deleted file mode 100644 index a7454322..00000000 --- a/java/templates/java-new-api-maven-spring-boot/.claude/CLAUDE.md +++ /dev/null @@ -1,408 +0,0 @@ -# Restate Java SDK Rules - -## Core Concepts - -* Restate provides durable execution: code automatically stores completed steps and resumes from where it left off on failures -* All handlers receive a `Context`/`ObjectContext`/`WorkflowContext`/`SharedObjectContext`/`SharedWorkflowContext` object as the first argument -* Handlers can take typed inputs and return typed outputs using Java classes and Jackson serialization - -## Service Types - -### Basic Services - -```java Java {"CODE_LOAD::java/src/main/java/develop/MyService.java#here"} theme={null} -import dev.restate.sdk.Context; -import dev.restate.sdk.annotation.Handler; -import dev.restate.sdk.annotation.Service; -import dev.restate.sdk.endpoint.Endpoint; -import dev.restate.sdk.http.vertx.RestateHttpServer; - -@Service -public class MyService { - @Handler - public String myHandler(Context ctx, String greeting) { - return greeting + "!"; - } - - public static void main(String[] args) { - RestateHttpServer.listen(Endpoint.bind(new MyService())); - } -} -``` - -### Virtual Objects (Stateful, Key-Addressable) - -```java Java {"CODE_LOAD::java/src/main/java/develop/MyObject.java#here"} theme={null} -import dev.restate.sdk.ObjectContext; -import dev.restate.sdk.SharedObjectContext; -import dev.restate.sdk.annotation.Handler; -import dev.restate.sdk.annotation.Shared; -import dev.restate.sdk.annotation.VirtualObject; -import dev.restate.sdk.endpoint.Endpoint; -import dev.restate.sdk.http.vertx.RestateHttpServer; - -@VirtualObject -public class MyObject { - - @Handler - public String myHandler(ObjectContext ctx, String greeting) { - String objectId = ctx.key(); - - return greeting + " " + objectId + "!"; - } - - @Shared - public String myConcurrentHandler(SharedObjectContext ctx, String input) { - return "my-output"; - } - - public static void main(String[] args) { - RestateHttpServer.listen(Endpoint.bind(new MyObject())); - } -} -``` - -### Workflows - -```java Java {"CODE_LOAD::java/src/main/java/develop/MyWorkflow.java#here"} theme={null} -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; -import dev.restate.sdk.http.vertx.RestateHttpServer; - -@Workflow -public class MyWorkflow { - - @Workflow - public String run(WorkflowContext ctx, String input) { - - // implement workflow logic here - - return "success"; - } - - @Shared - public String interactWithWorkflow(SharedWorkflowContext ctx, String input) { - // implement interaction logic here - return "my result"; - } - - public static void main(String[] args) { - RestateHttpServer.listen(Endpoint.bind(new MyWorkflow())); - } -} -``` - -## Context Operations - -### State Management (Virtual Objects & Workflows only) - -❌ Never use static variables - not durable, lost across replicas. -✅ Use `ctx.get()` and `ctx.set()` - durable and scoped to the object's key. - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#state"} theme={null} -// Get state keys -Collection keys = ctx.stateKeys(); - -// Get state -StateKey STRING_STATE_KEY = StateKey.of("my-key", String.class); -String stringState = ctx.get(STRING_STATE_KEY).orElse("my-default"); - -StateKey INT_STATE_KEY = StateKey.of("count", Integer.class); -int count = ctx.get(INT_STATE_KEY).orElse(0); - -// Set state -ctx.set(STRING_STATE_KEY, "my-new-value"); -ctx.set(INT_STATE_KEY, count + 1); - -// Clear state -ctx.clear(STRING_STATE_KEY); -ctx.clearAll(); -``` - -### Service Communication - -#### Request-Response - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#service_calls"} theme={null} -// Call a Service -String svcResponse = MyServiceClient.fromContext(ctx).myHandler(request).await(); - -// Call a Virtual Object -String objResponse = MyObjectClient.fromContext(ctx, objectKey).myHandler(request).await(); - -// Call a Workflow -String wfResponse = MyWorkflowClient.fromContext(ctx, workflowId).run(request).await(); -``` - -#### Generic Calls - -Call a service without using the generated client, but just String names. - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#generic_calls"} theme={null} -// Generic service call -Target target = Target.service("MyService", "myHandler"); -String response = - ctx.call(Request.of(target, TypeTag.of(String.class), TypeTag.of(String.class), request)) - .await(); - -// Generic object call -Target objectTarget = Target.virtualObject("MyObject", "object-key", "myHandler"); -String objResponse = - ctx.call( - Request.of( - objectTarget, TypeTag.of(String.class), TypeTag.of(String.class), request)) - .await(); - -// Generic workflow call -Target workflowTarget = Target.workflow("MyWorkflow", "wf-id", "run"); -String wfResponse = - ctx.call( - Request.of( - workflowTarget, TypeTag.of(String.class), TypeTag.of(String.class), request)) - .await(); -``` - -#### One-Way Messages - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#sending_messages"} theme={null} -// Call a Service -MyServiceClient.fromContext(ctx).send().myHandler(request); - -// Call a Virtual Object -MyObjectClient.fromContext(ctx, objectKey).send().myHandler(request); - -// Call a Workflow -MyWorkflowClient.fromContext(ctx, workflowId).send().run(request); -``` - -#### Delayed Messages - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#delayed_messages"} theme={null} -MyServiceClient.fromContext(ctx).send().myHandler(request, Duration.ofDays(5)); -``` - -#### With Idempotency Key - -```java theme={null} -Client restateClient = Client.connect("http://localhost:8080"); -MyServiceClient.fromClient(restateClient) - .send() - .myHandler("Hi", opt -> opt.idempotencyKey("my-key")); -``` - -### Run Actions or Side Effects (Non-Deterministic Operations) - -❌ Never call external APIs/DBs directly - will re-execute during replay, causing duplicates. -✅ Wrap in `ctx.run()` - Restate journals the result; runs only once. - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#durable_steps"} theme={null} -// Wrap non-deterministic code in ctx.run -String result = ctx.run("call external API", String.class, () -> callExternalAPI()); - -// Wrap with name for better tracing -String namedResult = ctx.run("my-side-effect", String.class, () -> callExternalAPI()); -``` - -### Deterministic randoms and time - -❌ Never use `Math.random()` - non-deterministic and breaks replay logic. -✅ Use `ctx.random()` or `ctx.uuid()` - Restate journals the result for deterministic replay. - -❌ Never use `System.currentTimeMillis()`, `new Date()` - returns different values during replay. -✅ Use `ctx.timer()` - Restate records and replays the same timestamp. - -### Durable Timers and Sleep - -❌ Never use `Thread.sleep()` or `CompletableFuture.delayedExecutor()` - not durable, lost on restarts. -✅ Use `ctx.sleep()` - durable timer that survives failures. - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#durable_timers"} theme={null} -// Sleep -ctx.sleep(Duration.ofSeconds(30)); - -// Schedule delayed call (different from sleep + send) -Target target = Target.service("MyService", "myHandler"); -ctx.send( - Request.of(target, TypeTag.of(String.class), TypeTag.of(String.class), "Hi"), - Duration.ofHours(5)); -``` - -### Awakeables (External Events) - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#awakeables"} theme={null} -// Create awakeable -Awakeable awakeable = ctx.awakeable(String.class); -String awakeableId = awakeable.id(); - -// Send ID to external system -ctx.run(() -> requestHumanReview(name, awakeableId)); - -// Wait for result -String review = awakeable.await(); - -// Resolve from another handler -ctx.awakeableHandle(awakeableId).resolve(String.class, "Looks good!"); - -// Reject from another handler -ctx.awakeableHandle(awakeableId).reject("Cannot be reviewed"); -``` - -### Durable Promises (Workflows only) - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#workflow_promises"} theme={null} -DurablePromiseKey REVIEW_PROMISE = DurablePromiseKey.of("review", String.class); -// Wait for promise -String review = ctx.promise(REVIEW_PROMISE).future().await(); - -// Resolve promise from another handler -ctx.promiseHandle(REVIEW_PROMISE).resolve(review); -``` - -## Concurrency - -Always use Restate combinators (`DurableFuture.all`, `DurableFuture.any`) instead of Java's native `CompletableFuture` methods - they journal execution order for deterministic replay. - -### `DurableFuture.all()` - Wait for All - -Returns when all futures complete. Use to wait for multiple operations to finish. - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#combine_all"} theme={null} -// Wait for all to complete -DurableFuture call1 = MyServiceClient.fromContext(ctx).myHandler("request1"); -DurableFuture call2 = MyServiceClient.fromContext(ctx).myHandler("request2"); - -DurableFuture.all(call1, call2).await(); -``` - -### `DurableFuture.any()` - First Successful Result - -Returns the first successful result, ignoring rejections until all fail. - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#combine_any"} theme={null} -// Wait for any to complete -int indexCompleted = DurableFuture.any(call1, call2).await(); -``` - -### Invocation Management - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#cancel"} theme={null} -var handle = - MyServiceClient.fromContext(ctx) - .send() - .myHandler(request, req -> req.idempotencyKey("abc123")); -var response = handle.attach().await(); -// Cancel invocation -handle.cancel(); -``` - -## Serialization - -### Default (Jackson JSON) - -By default, Java SDK uses Jackson for JSON serialization with POJOs. - -```java Java {"CODE_LOAD::java/src/main/java/develop/SerializationExample.java#state_keys"} theme={null} -// Primitive types -var myString = StateKey.of("myString", String.class); -// Generic types need TypeRef (similar to Jackson's TypeReference) -var myMap = StateKey.of("myMap", TypeTag.of(new TypeRef>() {})); -``` - -### Custom Serialization - -```java {"CODE_LOAD::java/src/main/java/develop/SerializationExample.java#customserdes"} theme={null} -class MyPersonSerde implements Serde { - @Override - public Slice serialize(Person person) { - // convert value to a byte array, then wrap in a Slice - return Slice.wrap(person.toBytes()); - } - - @Override - public Person deserialize(Slice slice) { - // convert value to Person - return Person.fromBytes(slice.toByteArray()); - } -} -``` - -And then use it, for example, in combination with `ctx.run`: - -```java {"CODE_LOAD::java/src/main/java/develop/SerializationExample.java#use_person_serde"} theme={null} -ctx.run(new MyPersonSerde(), () -> new Person()); -``` - -## Error Handling - -Restate retries failures indefinitely by default. For permanent business-logic failures (invalid input, declined payment), use TerminalException to stop retries immediately. - -### Terminal Errors (No Retry) - -```java Java {"CODE_LOAD::java/src/main/java/develop/ErrorHandling.java#here"} theme={null} -throw new TerminalException(500, "Something went wrong"); -``` - -### Retryable Errors - -```java theme={null} -// Any other thrown exception will be retried -throw new RuntimeException("Temporary failure - will retry"); -``` - -## Testing - -```java Java {"CODE_LOAD::java/src/main/java/develop/MyServiceTestMethod.java"} theme={null} -package develop; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import dev.restate.client.Client; -import dev.restate.sdk.testing.BindService; -import dev.restate.sdk.testing.RestateClient; -import dev.restate.sdk.testing.RestateTest; -import org.junit.jupiter.api.Test; - -@RestateTest -class MyServiceTestMethod { - - @BindService MyService service = new MyService(); - - @Test - void testMyHandler(@RestateClient Client ingressClient) { - // Create the service client from the injected ingress client - var client = MyServiceClient.fromClient(ingressClient); - - // Send request to service and assert the response - var response = client.myHandler("Hi"); - assertEquals(response, "Hi!"); - } -} -``` - -## SDK Clients (External Invocations) - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Clients.java#here"} theme={null} -Client restateClient = Client.connect("http://localhost:8080"); - -// Request-response -String result = MyServiceClient.fromClient(restateClient).myHandler("Hi"); - -// One-way -MyServiceClient.fromClient(restateClient).send().myHandler("Hi"); - -// Delayed -MyServiceClient.fromClient(restateClient).send().myHandler("Hi", Duration.ofSeconds(1)); - -// With idempotency key -MyObjectClient.fromClient(restateClient, "Mary") - .send() - .myHandler("Hi", opt -> opt.idempotencyKey("abc")); -``` - - ---- - -> To find navigation and other pages in this documentation, fetch the llms.txt file at: https://docs.restate.dev/llms.txt \ No newline at end of file diff --git a/java/templates/java-new-api-maven-spring-boot/.claude/CLAUDE.md b/java/templates/java-new-api-maven-spring-boot/.claude/CLAUDE.md new file mode 120000 index 00000000..be77ac83 --- /dev/null +++ b/java/templates/java-new-api-maven-spring-boot/.claude/CLAUDE.md @@ -0,0 +1 @@ +../AGENTS.md \ No newline at end of file diff --git a/java/templates/java-new-api-maven-spring-boot/.cursor/mcp.json b/java/templates/java-new-api-maven-spring-boot/.cursor/mcp.json deleted file mode 100644 index 78a21c5a..00000000 --- a/java/templates/java-new-api-maven-spring-boot/.cursor/mcp.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "mcpServers": { - "restate-docs": { - "url": "https://docs.restate.dev/mcp" - } - } -} \ No newline at end of file diff --git a/java/templates/java-new-api-maven-spring-boot/.cursor/mcp.json b/java/templates/java-new-api-maven-spring-boot/.cursor/mcp.json new file mode 120000 index 00000000..3aa24b56 --- /dev/null +++ b/java/templates/java-new-api-maven-spring-boot/.cursor/mcp.json @@ -0,0 +1 @@ +../.ai/mcp/mcp.json \ No newline at end of file diff --git a/java/templates/java-new-api-maven-spring-boot/.junie/guidelines.md b/java/templates/java-new-api-maven-spring-boot/.junie/guidelines.md new file mode 120000 index 00000000..be77ac83 --- /dev/null +++ b/java/templates/java-new-api-maven-spring-boot/.junie/guidelines.md @@ -0,0 +1 @@ +../AGENTS.md \ No newline at end of file diff --git a/java/templates/java-new-api-maven-spring-boot/.junie/mcp/mcp.json b/java/templates/java-new-api-maven-spring-boot/.junie/mcp/mcp.json new file mode 120000 index 00000000..f454b32d --- /dev/null +++ b/java/templates/java-new-api-maven-spring-boot/.junie/mcp/mcp.json @@ -0,0 +1 @@ +../../.ai/mcp/mcp.json \ No newline at end of file diff --git a/java/templates/java-new-api-maven-spring-boot/.cursor/rules/AGENTS.md b/java/templates/java-new-api-maven-spring-boot/AGENTS.md similarity index 100% rename from java/templates/java-new-api-maven-spring-boot/.cursor/rules/AGENTS.md rename to java/templates/java-new-api-maven-spring-boot/AGENTS.md diff --git a/java/templates/java-new-api-maven/.ai/mcp/mcp.json b/java/templates/java-new-api-maven/.ai/mcp/mcp.json new file mode 100644 index 00000000..78a21c5a --- /dev/null +++ b/java/templates/java-new-api-maven/.ai/mcp/mcp.json @@ -0,0 +1,7 @@ +{ + "mcpServers": { + "restate-docs": { + "url": "https://docs.restate.dev/mcp" + } + } +} \ No newline at end of file diff --git a/java/templates/java-new-api-maven/.aiassistant/rules/restate.md b/java/templates/java-new-api-maven/.aiassistant/rules/restate.md new file mode 120000 index 00000000..b7e6491d --- /dev/null +++ b/java/templates/java-new-api-maven/.aiassistant/rules/restate.md @@ -0,0 +1 @@ +../../AGENTS.md \ No newline at end of file diff --git a/java/templates/java-new-api-maven/.claude/CLAUDE.md b/java/templates/java-new-api-maven/.claude/CLAUDE.md deleted file mode 100644 index a7454322..00000000 --- a/java/templates/java-new-api-maven/.claude/CLAUDE.md +++ /dev/null @@ -1,408 +0,0 @@ -# Restate Java SDK Rules - -## Core Concepts - -* Restate provides durable execution: code automatically stores completed steps and resumes from where it left off on failures -* All handlers receive a `Context`/`ObjectContext`/`WorkflowContext`/`SharedObjectContext`/`SharedWorkflowContext` object as the first argument -* Handlers can take typed inputs and return typed outputs using Java classes and Jackson serialization - -## Service Types - -### Basic Services - -```java Java {"CODE_LOAD::java/src/main/java/develop/MyService.java#here"} theme={null} -import dev.restate.sdk.Context; -import dev.restate.sdk.annotation.Handler; -import dev.restate.sdk.annotation.Service; -import dev.restate.sdk.endpoint.Endpoint; -import dev.restate.sdk.http.vertx.RestateHttpServer; - -@Service -public class MyService { - @Handler - public String myHandler(Context ctx, String greeting) { - return greeting + "!"; - } - - public static void main(String[] args) { - RestateHttpServer.listen(Endpoint.bind(new MyService())); - } -} -``` - -### Virtual Objects (Stateful, Key-Addressable) - -```java Java {"CODE_LOAD::java/src/main/java/develop/MyObject.java#here"} theme={null} -import dev.restate.sdk.ObjectContext; -import dev.restate.sdk.SharedObjectContext; -import dev.restate.sdk.annotation.Handler; -import dev.restate.sdk.annotation.Shared; -import dev.restate.sdk.annotation.VirtualObject; -import dev.restate.sdk.endpoint.Endpoint; -import dev.restate.sdk.http.vertx.RestateHttpServer; - -@VirtualObject -public class MyObject { - - @Handler - public String myHandler(ObjectContext ctx, String greeting) { - String objectId = ctx.key(); - - return greeting + " " + objectId + "!"; - } - - @Shared - public String myConcurrentHandler(SharedObjectContext ctx, String input) { - return "my-output"; - } - - public static void main(String[] args) { - RestateHttpServer.listen(Endpoint.bind(new MyObject())); - } -} -``` - -### Workflows - -```java Java {"CODE_LOAD::java/src/main/java/develop/MyWorkflow.java#here"} theme={null} -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; -import dev.restate.sdk.http.vertx.RestateHttpServer; - -@Workflow -public class MyWorkflow { - - @Workflow - public String run(WorkflowContext ctx, String input) { - - // implement workflow logic here - - return "success"; - } - - @Shared - public String interactWithWorkflow(SharedWorkflowContext ctx, String input) { - // implement interaction logic here - return "my result"; - } - - public static void main(String[] args) { - RestateHttpServer.listen(Endpoint.bind(new MyWorkflow())); - } -} -``` - -## Context Operations - -### State Management (Virtual Objects & Workflows only) - -❌ Never use static variables - not durable, lost across replicas. -✅ Use `ctx.get()` and `ctx.set()` - durable and scoped to the object's key. - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#state"} theme={null} -// Get state keys -Collection keys = ctx.stateKeys(); - -// Get state -StateKey STRING_STATE_KEY = StateKey.of("my-key", String.class); -String stringState = ctx.get(STRING_STATE_KEY).orElse("my-default"); - -StateKey INT_STATE_KEY = StateKey.of("count", Integer.class); -int count = ctx.get(INT_STATE_KEY).orElse(0); - -// Set state -ctx.set(STRING_STATE_KEY, "my-new-value"); -ctx.set(INT_STATE_KEY, count + 1); - -// Clear state -ctx.clear(STRING_STATE_KEY); -ctx.clearAll(); -``` - -### Service Communication - -#### Request-Response - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#service_calls"} theme={null} -// Call a Service -String svcResponse = MyServiceClient.fromContext(ctx).myHandler(request).await(); - -// Call a Virtual Object -String objResponse = MyObjectClient.fromContext(ctx, objectKey).myHandler(request).await(); - -// Call a Workflow -String wfResponse = MyWorkflowClient.fromContext(ctx, workflowId).run(request).await(); -``` - -#### Generic Calls - -Call a service without using the generated client, but just String names. - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#generic_calls"} theme={null} -// Generic service call -Target target = Target.service("MyService", "myHandler"); -String response = - ctx.call(Request.of(target, TypeTag.of(String.class), TypeTag.of(String.class), request)) - .await(); - -// Generic object call -Target objectTarget = Target.virtualObject("MyObject", "object-key", "myHandler"); -String objResponse = - ctx.call( - Request.of( - objectTarget, TypeTag.of(String.class), TypeTag.of(String.class), request)) - .await(); - -// Generic workflow call -Target workflowTarget = Target.workflow("MyWorkflow", "wf-id", "run"); -String wfResponse = - ctx.call( - Request.of( - workflowTarget, TypeTag.of(String.class), TypeTag.of(String.class), request)) - .await(); -``` - -#### One-Way Messages - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#sending_messages"} theme={null} -// Call a Service -MyServiceClient.fromContext(ctx).send().myHandler(request); - -// Call a Virtual Object -MyObjectClient.fromContext(ctx, objectKey).send().myHandler(request); - -// Call a Workflow -MyWorkflowClient.fromContext(ctx, workflowId).send().run(request); -``` - -#### Delayed Messages - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#delayed_messages"} theme={null} -MyServiceClient.fromContext(ctx).send().myHandler(request, Duration.ofDays(5)); -``` - -#### With Idempotency Key - -```java theme={null} -Client restateClient = Client.connect("http://localhost:8080"); -MyServiceClient.fromClient(restateClient) - .send() - .myHandler("Hi", opt -> opt.idempotencyKey("my-key")); -``` - -### Run Actions or Side Effects (Non-Deterministic Operations) - -❌ Never call external APIs/DBs directly - will re-execute during replay, causing duplicates. -✅ Wrap in `ctx.run()` - Restate journals the result; runs only once. - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#durable_steps"} theme={null} -// Wrap non-deterministic code in ctx.run -String result = ctx.run("call external API", String.class, () -> callExternalAPI()); - -// Wrap with name for better tracing -String namedResult = ctx.run("my-side-effect", String.class, () -> callExternalAPI()); -``` - -### Deterministic randoms and time - -❌ Never use `Math.random()` - non-deterministic and breaks replay logic. -✅ Use `ctx.random()` or `ctx.uuid()` - Restate journals the result for deterministic replay. - -❌ Never use `System.currentTimeMillis()`, `new Date()` - returns different values during replay. -✅ Use `ctx.timer()` - Restate records and replays the same timestamp. - -### Durable Timers and Sleep - -❌ Never use `Thread.sleep()` or `CompletableFuture.delayedExecutor()` - not durable, lost on restarts. -✅ Use `ctx.sleep()` - durable timer that survives failures. - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#durable_timers"} theme={null} -// Sleep -ctx.sleep(Duration.ofSeconds(30)); - -// Schedule delayed call (different from sleep + send) -Target target = Target.service("MyService", "myHandler"); -ctx.send( - Request.of(target, TypeTag.of(String.class), TypeTag.of(String.class), "Hi"), - Duration.ofHours(5)); -``` - -### Awakeables (External Events) - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#awakeables"} theme={null} -// Create awakeable -Awakeable awakeable = ctx.awakeable(String.class); -String awakeableId = awakeable.id(); - -// Send ID to external system -ctx.run(() -> requestHumanReview(name, awakeableId)); - -// Wait for result -String review = awakeable.await(); - -// Resolve from another handler -ctx.awakeableHandle(awakeableId).resolve(String.class, "Looks good!"); - -// Reject from another handler -ctx.awakeableHandle(awakeableId).reject("Cannot be reviewed"); -``` - -### Durable Promises (Workflows only) - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#workflow_promises"} theme={null} -DurablePromiseKey REVIEW_PROMISE = DurablePromiseKey.of("review", String.class); -// Wait for promise -String review = ctx.promise(REVIEW_PROMISE).future().await(); - -// Resolve promise from another handler -ctx.promiseHandle(REVIEW_PROMISE).resolve(review); -``` - -## Concurrency - -Always use Restate combinators (`DurableFuture.all`, `DurableFuture.any`) instead of Java's native `CompletableFuture` methods - they journal execution order for deterministic replay. - -### `DurableFuture.all()` - Wait for All - -Returns when all futures complete. Use to wait for multiple operations to finish. - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#combine_all"} theme={null} -// Wait for all to complete -DurableFuture call1 = MyServiceClient.fromContext(ctx).myHandler("request1"); -DurableFuture call2 = MyServiceClient.fromContext(ctx).myHandler("request2"); - -DurableFuture.all(call1, call2).await(); -``` - -### `DurableFuture.any()` - First Successful Result - -Returns the first successful result, ignoring rejections until all fail. - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#combine_any"} theme={null} -// Wait for any to complete -int indexCompleted = DurableFuture.any(call1, call2).await(); -``` - -### Invocation Management - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Actions.java#cancel"} theme={null} -var handle = - MyServiceClient.fromContext(ctx) - .send() - .myHandler(request, req -> req.idempotencyKey("abc123")); -var response = handle.attach().await(); -// Cancel invocation -handle.cancel(); -``` - -## Serialization - -### Default (Jackson JSON) - -By default, Java SDK uses Jackson for JSON serialization with POJOs. - -```java Java {"CODE_LOAD::java/src/main/java/develop/SerializationExample.java#state_keys"} theme={null} -// Primitive types -var myString = StateKey.of("myString", String.class); -// Generic types need TypeRef (similar to Jackson's TypeReference) -var myMap = StateKey.of("myMap", TypeTag.of(new TypeRef>() {})); -``` - -### Custom Serialization - -```java {"CODE_LOAD::java/src/main/java/develop/SerializationExample.java#customserdes"} theme={null} -class MyPersonSerde implements Serde { - @Override - public Slice serialize(Person person) { - // convert value to a byte array, then wrap in a Slice - return Slice.wrap(person.toBytes()); - } - - @Override - public Person deserialize(Slice slice) { - // convert value to Person - return Person.fromBytes(slice.toByteArray()); - } -} -``` - -And then use it, for example, in combination with `ctx.run`: - -```java {"CODE_LOAD::java/src/main/java/develop/SerializationExample.java#use_person_serde"} theme={null} -ctx.run(new MyPersonSerde(), () -> new Person()); -``` - -## Error Handling - -Restate retries failures indefinitely by default. For permanent business-logic failures (invalid input, declined payment), use TerminalException to stop retries immediately. - -### Terminal Errors (No Retry) - -```java Java {"CODE_LOAD::java/src/main/java/develop/ErrorHandling.java#here"} theme={null} -throw new TerminalException(500, "Something went wrong"); -``` - -### Retryable Errors - -```java theme={null} -// Any other thrown exception will be retried -throw new RuntimeException("Temporary failure - will retry"); -``` - -## Testing - -```java Java {"CODE_LOAD::java/src/main/java/develop/MyServiceTestMethod.java"} theme={null} -package develop; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import dev.restate.client.Client; -import dev.restate.sdk.testing.BindService; -import dev.restate.sdk.testing.RestateClient; -import dev.restate.sdk.testing.RestateTest; -import org.junit.jupiter.api.Test; - -@RestateTest -class MyServiceTestMethod { - - @BindService MyService service = new MyService(); - - @Test - void testMyHandler(@RestateClient Client ingressClient) { - // Create the service client from the injected ingress client - var client = MyServiceClient.fromClient(ingressClient); - - // Send request to service and assert the response - var response = client.myHandler("Hi"); - assertEquals(response, "Hi!"); - } -} -``` - -## SDK Clients (External Invocations) - -```java {"CODE_LOAD::java/src/main/java/develop/agentsmd/Clients.java#here"} theme={null} -Client restateClient = Client.connect("http://localhost:8080"); - -// Request-response -String result = MyServiceClient.fromClient(restateClient).myHandler("Hi"); - -// One-way -MyServiceClient.fromClient(restateClient).send().myHandler("Hi"); - -// Delayed -MyServiceClient.fromClient(restateClient).send().myHandler("Hi", Duration.ofSeconds(1)); - -// With idempotency key -MyObjectClient.fromClient(restateClient, "Mary") - .send() - .myHandler("Hi", opt -> opt.idempotencyKey("abc")); -``` - - ---- - -> To find navigation and other pages in this documentation, fetch the llms.txt file at: https://docs.restate.dev/llms.txt \ No newline at end of file diff --git a/java/templates/java-new-api-maven/.claude/CLAUDE.md b/java/templates/java-new-api-maven/.claude/CLAUDE.md new file mode 120000 index 00000000..be77ac83 --- /dev/null +++ b/java/templates/java-new-api-maven/.claude/CLAUDE.md @@ -0,0 +1 @@ +../AGENTS.md \ No newline at end of file diff --git a/java/templates/java-new-api-maven/.cursor/mcp.json b/java/templates/java-new-api-maven/.cursor/mcp.json deleted file mode 100644 index 78a21c5a..00000000 --- a/java/templates/java-new-api-maven/.cursor/mcp.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "mcpServers": { - "restate-docs": { - "url": "https://docs.restate.dev/mcp" - } - } -} \ No newline at end of file diff --git a/java/templates/java-new-api-maven/.cursor/mcp.json b/java/templates/java-new-api-maven/.cursor/mcp.json new file mode 120000 index 00000000..3aa24b56 --- /dev/null +++ b/java/templates/java-new-api-maven/.cursor/mcp.json @@ -0,0 +1 @@ +../.ai/mcp/mcp.json \ No newline at end of file diff --git a/java/templates/java-new-api-maven/.junie/guidelines.md b/java/templates/java-new-api-maven/.junie/guidelines.md new file mode 120000 index 00000000..be77ac83 --- /dev/null +++ b/java/templates/java-new-api-maven/.junie/guidelines.md @@ -0,0 +1 @@ +../AGENTS.md \ No newline at end of file diff --git a/java/templates/java-new-api-maven/.junie/mcp/mcp.json b/java/templates/java-new-api-maven/.junie/mcp/mcp.json new file mode 120000 index 00000000..f454b32d --- /dev/null +++ b/java/templates/java-new-api-maven/.junie/mcp/mcp.json @@ -0,0 +1 @@ +../../.ai/mcp/mcp.json \ No newline at end of file diff --git a/java/templates/java-new-api-maven/.cursor/rules/AGENTS.md b/java/templates/java-new-api-maven/AGENTS.md similarity index 100% rename from java/templates/java-new-api-maven/.cursor/rules/AGENTS.md rename to java/templates/java-new-api-maven/AGENTS.md diff --git a/java/templates/java-new-api-maven/README.md b/java/templates/java-new-api-maven/README.md index 49ef7014..f032c39c 100644 --- a/java/templates/java-new-api-maven/README.md +++ b/java/templates/java-new-api-maven/README.md @@ -8,7 +8,4 @@ To run: ```shell mvn compile exec:java -``` - -Restate SDK uses annotation processing to generate client classes. -When modifying the annotated services in Intellij, it is suggested to run **CTRL + F9** to re-generate the client classes. \ No newline at end of file +``` \ No newline at end of file