Fetch the complete documentation index at: https://docs.restate.dev/llms.txt Use this file to discover all available pages before exploring further.
- 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/SharedWorkflowContextobject as the first argument - Handlers can take typed inputs and return typed outputs using Java classes and Jackson serialization
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()));
}
}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()));
}
}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()));
}
}❌ 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();// 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();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();// 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);MyServiceClient.fromContext(ctx).send().myHandler(request, Duration.ofDays(5));Client restateClient = Client.connect("http://localhost:8080");
MyServiceClient.fromClient(restateClient)
.send()
.myHandler("Hi", opt -> opt.idempotencyKey("my-key"));❌ 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());❌ 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.
❌ 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));// 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");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);Always use Restate combinators (DurableFuture.all, DurableFuture.any) instead of Java's native CompletableFuture methods - they journal execution order for deterministic replay.
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();Returns the first successful result, ignoring rejections until all fail.
// Wait for any to complete
int indexCompleted = DurableFuture.any(call1, call2).await();var handle =
MyServiceClient.fromContext(ctx)
.send()
.myHandler(request, req -> req.idempotencyKey("abc123"));
var response = handle.attach().await();
// Cancel invocation
handle.cancel();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>>() {}));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());Restate retries failures indefinitely by default. For permanent business-logic failures (invalid input, declined payment), use TerminalException to stop retries immediately.
throw new TerminalException(500, "Something went wrong");// Any other thrown exception will be retried
throw new RuntimeException("Temporary failure - will retry");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!");
}
}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"));