Skip to content

Latest commit

 

History

History
407 lines (298 loc) · 12.7 KB

File metadata and controls

407 lines (298 loc) · 12.7 KB

Documentation Index

Fetch the complete documentation index at: https://docs.restate.dev/llms.txt Use this file to discover all available pages before exploring further.

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

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)

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

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.

// Get state keys
Collection<String> keys = ctx.stateKeys();

// Get state
StateKey<String> STRING_STATE_KEY = StateKey.of("my-key", String.class);
String stringState = ctx.get(STRING_STATE_KEY).orElse("my-default");

StateKey<Integer> 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

// 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.

// 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

// 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

MyServiceClient.fromContext(ctx).send().myHandler(request, Duration.ofDays(5));

With Idempotency Key

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.

// 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.

// 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)

// Create awakeable
Awakeable<String> 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)

DurablePromiseKey<String> 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.

// Wait for all to complete
DurableFuture<String> call1 = MyServiceClient.fromContext(ctx).myHandler("request1");
DurableFuture<String> 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.

// Wait for any to complete
int indexCompleted = DurableFuture.any(call1, call2).await();

Invocation Management

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.

// 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<Map<String, String>>() {}));

Custom Serialization

class MyPersonSerde implements Serde<Person> {
  @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:

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)

throw new TerminalException(500, "Something went wrong");

Retryable Errors

// Any other thrown exception will be retried
throw new RuntimeException("Temporary failure - will retry");

Testing

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)

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"));