This document provides a comprehensive guide to developing resilient applications with the Restate TypeScript SDK. It covers fundamental concepts, advanced patterns, and practical examples to help you build robust and scalable systems.
Restate is a system designed to simplify the development of reliable, distributed applications. It provides durable execution, state management, and powerful primitives that turn familiar programming constructs into fault-tolerant, distributed building blocks. At its core, Restate ensures that your code, once invoked, runs to completion, even in the presence of failures.
- Durable Execution: Restate ensures that your code runs to completion by automatically retrying failed operations and recovering previously finished actions from a journal. This makes your business logic resilient to transient failures and infrastructure restarts without complex manual error handling.
- Services: The fundamental building blocks of a Restate application. They are stateless and contain handlers that implement your business logic. They can be invoked via RPC from other services or external clients.
- Virtual Objects: Stateful services with access to a built-in, consistent key-value store. Each object is keyed, and Restate guarantees that only one handler executes at a time for a given key, ensuring data consistency and preventing race conditions without manual locking. This is often referred to as the Virtual Actor model.
- Workflows: A specialized type of virtual object for orchestrating long-running, durable processes. They have a main
run
handler that is guaranteed to execute exactly once per workflow instance ID. They can be queried for their status, signaled with external events, and awaited for their result.
The following example demonstrates a service that durably executes a series of steps. If the service crashes, Restate will automatically restart it and replay its progress, ensuring that sendNotification
is not called again and sendReminder
is retried until it succeeds.
import * as restate from "@restatedev/restate-sdk";
import { sendNotification, sendReminder } from "./utils";
const greeter = restate.service({
name: "Greeter",
handlers: {
greet: async (ctx: restate.Context, name: string) => {
// Durably execute a set of steps; resilient against failures
const greetingId = ctx.rand.uuidv4();
await ctx.run("Notification", () => sendNotification(greetingId, name));
await ctx.sleep(1000); // Durably sleep for 1 second
await ctx.run("Reminder", () => sendReminder(greetingId, name));
// Respond to the caller
return `Hi ${name}!`;
},
},
});
restate.endpoint().bind(greeter).listen(9080);
The Context
object (ctx
) is the first parameter in every handler and is the primary interface for accessing Restate's features. Different handler types (service, object, workflow) receive specialized versions of the context with additional capabilities.
ctx.run()
: Executes a function durably, journaling its result to ensure it's not re-executed on retries. This is essential for all side effects.ctx.serviceClient()
: Creates a client for making reliable RPC calls to other services.ctx.console
: A logger that is aware of Restate's execution model. It automatically adds invocation context (like the invocation ID) to log messages and suppresses logs during retries to avoid noise.ctx.rand
: Provides deterministic random number and UUID generators, crucial for creating stable idempotency keys across retries.ctx.rand.random()
: LikeMath.random()
but deterministic.ctx.rand.uuidv4()
: Generates a deterministic UUID.
ctx.sleep()
: A durable sleep that can pause a handler's execution for a specified duration.ctx.awakeable()
: Creates a durable promise that can be resolved or rejected by an external system, enabling reliable callback patterns.ctx.promise()
: (Workflow-only) Creates a durable promise scoped to the workflow instance for signaling between handlers.
In addition to core features, ObjectContext
provides access to the virtual object's key-value state store.
ctx.key
: The unique key identifying the virtual object instance.ctx.get()
: Retrieves a value from the object's state.ctx.set()
: Sets a value in the object's state.ctx.clear()
: Deletes a key from the object's state.
Workflows have the most extensive context, combining the features of ObjectContext
with workflow-specific operations. Shared workflow handlers (signal
and query
) use WorkflowSharedContext
, which has a read-only view of state.
The cornerstone of Restate's resilience is durable execution. Any interaction with the outside world (APIs, databases, etc.) or any non-deterministic code must be wrapped in ctx.run()
to ensure correctness during retries.
When you wrap code in ctx.run()
, Restate does the following:
- On first execution: It executes the code block and journals the result (or the error).
- On success: It proceeds to the next step.
- On failure: The entire invocation is retried.
- On replay: Instead of re-executing the code block, Restate replays the journaled result, ensuring the handler's logic continues from a consistent state.
You can customize the retry behavior for ctx.run()
by providing a RunOptions
object. This is useful for handling external systems that might have specific rate limits or failure characteristics.
Example: A ctx.run()
with a custom retry policy
await ctx.run(
"Call a flaky API",
() => callFlakyApi(),
{
// These options configure an exponential backoff strategy.
initialRetryInterval: { seconds: 1 }, // Start with a 1-second delay
maxRetryInterval: { minutes: 1 }, // Cap the delay at 1 minute
maxRetryAttempts: 5, // Give up after 5 attempts
retryIntervalFactor: 2.0 // Double the delay after each attempt
}
);
If the action does not succeed after all attempts, ctx.run()
will throw a TerminalError
.
Restate applications are composed of one or more of these component types, each serving a different purpose.
A stateless component for implementing request-response logic.
export const checkoutService = restate.service({
name: "CheckoutService",
handlers: {
handle: async (ctx: restate.Context, request: { userId: string; tickets: string[] }) => {
// ... business logic
return true;
},
}
});
A stateful component, keyed by an ID, for modeling entities like user carts, tickets, or IoT devices.
export const ticketObject = restate.object({
name: "TicketObject",
handlers: {
reserve: async (ctx: restate.ObjectContext) => {
const status = await ctx.get<TicketStatus>("status") ?? TicketStatus.Available;
if (status === TicketStatus.Available) {
ctx.set("status", TicketStatus.Reserved);
return true;
}
return false;
},
// Read-only, concurrent handler
getStatus: restate.handlers.object.shared(async (ctx: restate.ObjectSharedContext) => {
return await ctx.get<TicketStatus>("status");
})
}
});
An orchestrator for long-running, multi-step processes.
export const signupWorkflow = restate.workflow({
name: "usersignup",
handlers: {
run: async (ctx: restate.WorkflowContext, user: { name: string; email: string }) => {
// ... workflow logic
},
click: async (ctx: restate.WorkflowSharedContext, request: { secret: string }) => {
// ... signal handler logic
},
},
});
Virtual objects and workflows provide a durable key-value store, accessed via the context (ctx
). Restate manages the persistence and consistency of this state.
ctx.get<T>(name: string): Promise<T | null>
: Retrieves the state for a given key.ctx.set<T>(name: string, value: T): void
: Sets the state for a key.ctx.clear(name: string): void
: Deletes a key.ctx.stateKeys(): Promise<string[]>
: Returns all keys for the current object.
By default, handlers in a virtual object are exclusive, meaning Restate ensures only one is executing for a given object key at any time. This serializes access and prevents race conditions.
You can declare a handler as shared using restate.handlers.object.shared()
. Shared handlers can execute concurrently but have read-only access to state (ctx.get()
is allowed, but ctx.set()
and ctx.clear()
are not). This is ideal for query-style methods.
Restate simplifies inter-service communication by making it durable and reliable.
Use a service client to make a request to another handler and await
its response. Restate guarantees the call will be delivered and executed at least once.
ctx.serviceClient()
ctx.objectClient()
ctx.workflowClient()
Example: Idempotent RPC
To ensure exactly-once semantics, especially when interacting with external systems, provide an idempotency key.
const products = restateClient.objectClient<ProductService>({ name: "product" }, productId);
const reservation = await products.reserve(
// Restate deduplicates requests with the same idempotency key
Opts.from({ idempotencyKey: reservationId })
);
Use a send client to fire-and-forget a message to another handler. Restate guarantees eventual delivery.
ctx.serviceSendClient()
ctx.objectSendClient()
ctx.workflowSendClient()
Example: Delayed One-Way Message
// Expire a ticket reservation after 15 minutes
ctx.objectSendClient(cartObject, ctx.key, { delay: { minutes: 15 } })
.expireTicket(ticketId);
This demonstrates how to use Restate as a durable, delayed task queue.
Restate provides powerful primitives for managing time and coordinating asynchronous operations.
ctx.sleep(duration)
creates a durable timer. The handler execution will suspend and resume after the specified duration, even across failures and restarts.
Awakeables bridge the gap between Restate and external asynchronous systems. A handler can create an awakeable, pass its id
to an external system, and await
its promise
. The external system can then use that ID to call back into Restate and resolve the promise.
Example: Stripe Webhook Integration
// In the payment handler:
const { id: intentWebhookId, promise: intentPromise } = ctx.awakeable<Stripe.PaymentIntent>();
const paymentIntent = await ctx.run("stripe call", () =>
stripe_utils.createPaymentIntent({
// ...
// Pass the awakeable ID in the payment metadata
intentWebhookId,
})
);
// If Stripe's response is "processing", we wait for the webhook
if (paymentIntent.status === "processing") {
const processedPaymentIntent = await intentPromise; // Await the webhook callback
// ... continue logic
}
// In the webhook handler:
const paymentIntent = event.data.object as Stripe.PaymentIntent;
const webhookPromiseId = paymentIntent.metadata.restate_callback_id;
if (webhookPromiseId) {
// Resolve the awakeable to resume the waiting handler
ctx.resolveAwakeable(webhookPromiseId, paymentIntent);
}
ctx.promise<T>(name)
(in a workflow context) creates a promise scoped to that workflow instance. It can be used for reliable signaling between the workflow's run
handler and its shared handlers.
Restate provides robust error handling capabilities. By default, any thrown error causes the invocation to be retried.
If an error is not transient and should not be retried (e.g., "invalid credit card"), you should throw a TerminalError
. This stops the retry loop and fails the invocation immediately.
if (!paid) {
// This will not be retried.
throw new restate.TerminalError("Payment was rejected.", { errorCode: 400 });
}
Other specialized terminal errors include TimeoutError
and CancelledError
.
The Saga pattern is a way to manage data consistency across multiple services in a distributed transaction. If a step in the process fails with a terminal error, previously completed steps must be compensated for (undone). Restate's durable execution makes implementing sagas straightforward.
Example: Trip Booking Saga
In this pattern, for each successful action (e.g., booking a flight), we add a corresponding compensating action (e.g., canceling the flight) to a list. If a later step fails terminally, we execute the compensating actions in reverse order.
const bookingWorkflow = restate.service({
name: "BookingWorkflow",
handlers: {
run: async (ctx: restate.Context, req: BookingRequest) => {
const compensations: (() => Promise<void>)[] = [];
try {
await ctx.run("Book flight", () => flightClient.book(req.customerId, req.flight));
compensations.push(() => ctx.run("Cancel flight", () => flightClient.cancel(req.customerId)));
await ctx.run("Book car", () => carRentalClient.book(req.customerId, req.car));
compensations.push(() => ctx.run("Cancel car", () => carRentalClient.cancel(req.customerId)));
// This hotel booking will fail terminally
await ctx.run("Book hotel", () => hotelClient.book(req.customerId, req.hotel));
compensations.push(() => ctx.run("Cancel hotel", () => hotelClient.cancel(req.customerId)));
} catch (e) {
if (e instanceof restate.TerminalError) {
// Execute all compensations in reverse order
for (const compensation of compensations.reverse()) {
await compensation();
}
}
throw e;
}
},
},
});
Restate can serve as a sink for Kafka topics, enabling you to build robust, transactional event processing applications. The key from the Kafka message is used to route the event to a specific virtual object instance, ensuring ordered, sequential processing for that key.
You configure Restate to consume from a Kafka topic and invoke a specific service handler. This is done via the Restate admin API or a configuration file.
Example: restate.toml
[[ingress.kafka-clusters]]
name = "my-cluster"
brokers = ["PLAINTEXT://kafka:9092"]
Example: Subscribing a Handler
curl localhost:9070/subscriptions -H 'content-type: application/json' \
-d '{
"source": "kafka://my-cluster/social-media-posts",
"sink": "service://userFeed/processPost"
}'
While Restate doesn't have a direct Kafka producer API in the SDK, you can publish events to Kafka from within a ctx.run()
block using a standard Kafka client library like kafkajs
. This ensures the message is sent durably as part of the invocation's transactional guarantees. The food-ordering
example demonstrates this pattern by having a simulated driver app publish location updates.
// From food-ordering/app/src/order-app/clients/kafka_publisher.ts
import { Kafka } from "kafkajs";
export class Kafka_publisher {
// ... Kafka producer setup ...
public async send(driverId: string, location: Location) {
// ... connect logic ...
await this.producer.send({
topic: "driver-updates",
messages: [{key: driverId, value: JSON.stringify(location)}],
});
}
}
// The send call is wrapped in ctx.run() within the handler
// From food-ordering/app/src/order-app/external/driver_mobile_app_sim.ts
await ctx.run(() => kafkaPublisher.send(ctx.key, location));
External applications, such as a web frontend or another backend service, can interact with Restate services using the @restatedev/restate-sdk-clients
package.
The client provides a typesafe way to invoke handlers.
Example: Calling a Workflow from an external script
import * as restate from "@restatedev/restate-sdk-clients";
import type { SignupApi } from "./3_workflows.ts";
async function signupUser(userId: string, name: string, email: string) {
const rs = restate.connect({ url: "http://localhost:8080" });
const workflowClient = rs.workflowClient<SignupApi>({ name: "usersignup" }, userId);
const response = await workflowClient.workflowSubmit({ name, email });
if (response.status != "Accepted") {
throw new Error("User ID already taken");
}
// Await the final result of the workflow
const userVerified = await workflowClient.workflowAttach();
}
When calling from external systems, it's crucial to use idempotency keys to prevent duplicate operations in case of client-side retries or network issues.
const reservation = await products.reserve({
idempotencyKey: reservationId // A unique key for this specific operation
});
The @restatedev/restate-sdk-testcontainers
package simplifies end-to-end testing by providing a way to programmatically start and manage a Restate server instance within your tests.
RestateTestEnvironment
starts a Docker container with the Restate server and binds your services to it.
Example: A Jest Test for a Virtual Object
import { RestateTestEnvironment } from "@restatedev/restate-sdk-testcontainers";
import { exampleObject } from "../src/example_object";
import * as clients from "@restatedev/restate-sdk-clients";
describe("ExampleObject", () => {
let restateTestEnvironment: RestateTestEnvironment;
let restateIngress: clients.Ingress;
// Start the Restate environment before all tests
beforeAll(async () => {
restateTestEnvironment = await RestateTestEnvironment.start((restateServer) =>
restateServer.bind(exampleObject)
);
restateIngress = clients.connect({ url: restateTestEnvironment.baseUrl() });
}, 20_000);
// Stop the environment after all tests
afterAll(async () => {
await restateTestEnvironment.stop();
});
it("should increment the counter correctly", async () => {
// Directly manipulate the object's state for test setup
const state = restateTestEnvironment.stateOf(exampleObject, "Sarah");
await state.set("count", 123);
// Invoke the handler
const greet = await restateIngress.objectClient(exampleObject, "Sarah").greet();
// Assert the result and the new state
expect(greet).toBe("Hello Sarah! Counter: 123");
expect(await state.get("count")).toBe(124);
});
});
This approach allows you to test the full, durable behavior of your application in an isolated environment.
Restate services can be deployed in various environments.
The simplest way to deploy is as a standalone Node.js application using the built-in HTTP2 server.
// From templates/node/src/app.ts
import * as restate from "@restatedev/restate-sdk";
// ... service definitions ...
restate.endpoint().bind(greeter).listen(9080);
This can then be containerized using a Dockerfile
.
Restate services can be deployed as AWS Lambda functions. The @restatedev/restate-cdk
package provides constructs to simplify deploying both your service and a self-hosted Restate server or connecting to Restate Cloud.
Example: CDK Stack
This CDK stack deploys a Lambda function and registers it with a Restate Cloud environment.
// From integrations/deployment-lambda-cdk/lib/lambda-ts-cdk-stack.ts
import * as restate from "@restatedev/restate-cdk";
import * as cdk from "aws-cdk-lib";
import * as lambda from "aws-cdk-lib/aws-lambda";
// ...
export class LambdaTsCdkStack extends cdk.Stack {
constructor(scope: Construct, id: string, props: cdk.StackProps) {
super(scope, id, props);
// Define the Lambda function
const handler = new lambda_nodejs.NodejsFunction(this, "GreeterService", {
// ... lambda configuration ...
entry: "lib/lambda/handler.ts",
});
// Connect to your Restate Cloud environment
const restateEnvironment = new restate.RestateCloudEnvironment(this, "RestateCloud", {
environmentId: process.env.RESTATE_ENV_ID! as restate.EnvironmentId,
apiKey: new secrets.Secret(this, "RestateCloudApiKey", {
secretStringValue: cdk.SecretValue.unsafePlainText(process.env.RESTATE_API_KEY!),
}),
});
// Deploy the service to the environment
const deployer = new restate.ServiceDeployer(this, "ServiceDeployer");
deployer.deployService("Greeter", handler.currentVersion, restateEnvironment);
}
}
Understanding these core types is key to effectively using the SDK.
-
Serde
(Serializer/Deserializer): Defines how data is converted to and fromUint8Array
for communication with Restate. The SDK provides a default JSON serde (restate.serde.json
), a binary serde (restate.serde.binary
), and the ability to define custom serdes. It is used in handler definitions and client calls to specify data formats.restate.serde.json<T>()
: For any JSON-serializable type.restate.serde.binary()
: For rawUint8Array
payloads.restate.serde.string()
: For plain strings.restate.serde.empty()
: For void/empty payloads.
-
Duration
: Represents a time duration, used forctx.sleep()
and delayed calls. It can be created from milliseconds, seconds, minutes, etc. Example:restate.Duration.fromMinutes(15)
. A simplenumber
can also be used, which is interpreted as milliseconds. -
RestateError
: The base class for all errors originating from the Restate SDK. -
TerminalError
: A subclass ofRestateError
that signals a non-retriable failure. When a handler throws aTerminalError
, Restate will not attempt to retry the invocation and will immediately fail the operation. -
TimeoutError
andCancelledError
: Specific types ofTerminalError
thrown when aRestatePromise
times out (via.orTimeout()
) or when an invocation is cancelled. -
Wasm*
types (e.g.,WasmVM
,WasmInput
,WasmFailure
): These are low-level types from the WebAssembly core (sdk_shared_core_wasm_bindings.d.ts
). You will typically not interact with these directly. They represent the boundary between the TypeScript SDK and the underlying Rust-based state machine, managing the protocol and journaling logic. TheGenericHandler
uses these types to process the raw invocation stream from the Restate server.