Skip to main content

Services

This is what a Restate application looks like from a helicopter view:

Application overview
  1. Restate Server: The server intercepts incoming requests and drives their execution till the end.
  2. Services: Contain the handlers which process incoming requests.
  3. Invocation: A request to execute a handler that is part of either a service, or a virtual object.

As you can see, handlers are bundled into services. Services run like regular RPC services (e.g. a NodeJS app in a Docker container). Services can be written in any language for which there is an SDK available.

There are three types of services in Restate:

Services (plain)Virtual objectsWorkflows
Set of handlers durably executedSet of handlers durably executedThe workflow run handler is durably executed a single time.
No concurrency limitsSingle concurrent invocation per virtual objectThe run handler can run only a single time. Other handlers can run concurrently to interact with the workflow.
No associated K/V storeHandlers share K/V state; isolated per virtual objectK/V state isolated per workflow execution. Can only be set by the run handler.
Example use cases:
  • Microservice orchestration
  • To benefit from idempotency
  • Transformation functions
  • Sagas
Example use cases:
  • Atomic state machines
  • Digital twin
  • Locking mechanisms
  • Sequencing or ordering invocations
Example use cases:
  • Order processing and logistics
  • User sign-up workflow
  • Infrastructure provisioning
  • Workflow interpreters

Services

Services expose a collection of handlers:

Restate makes sure that handlers run to completion, even in the presence of failures. Restate logs the results of actions in the system. Restate takes care of retries and recovers the handler to the point where it failed.


import * as restate from "@restatedev/restate-sdk";
import { Context } from "@restatedev/restate-sdk";
export const roleUpdateService = restate.service({
name: "roleUpdate",
handlers: {
applyRoleUpdate: async (ctx: Context, update: UpdateRequest) => {
const { userId, role, permissions } = update;
const success = await ctx.run(() => applyUserRole(userId, role));
if (!success) {
return;
}
for (const permission of permissions) {
await ctx.run(() => applyPermission(userId, permission));
}
},
},
});
restate.endpoint().bind(roleUpdateService).listen();

The handlers of services are independent and can be invoked concurrently.


import * as restate from "@restatedev/restate-sdk";
import { Context } from "@restatedev/restate-sdk";
export const roleUpdateService = restate.service({
name: "roleUpdate",
handlers: {
applyRoleUpdate: async (ctx: Context, update: UpdateRequest) => {
const { userId, role, permissions } = update;
const success = await ctx.run(() => applyUserRole(userId, role));
if (!success) {
return;
}
for (const permission of permissions) {
await ctx.run(() => applyPermission(userId, permission));
}
},
},
});
restate.endpoint().bind(roleUpdateService).listen();

Handlers use the regular code and control flow, no custom DSLs.


import * as restate from "@restatedev/restate-sdk";
import { Context } from "@restatedev/restate-sdk";
export const roleUpdateService = restate.service({
name: "roleUpdate",
handlers: {
applyRoleUpdate: async (ctx: Context, update: UpdateRequest) => {
const { userId, role, permissions } = update;
const success = await ctx.run(() => applyUserRole(userId, role));
if (!success) {
return;
}
for (const permission of permissions) {
await ctx.run(() => applyPermission(userId, permission));
}
},
},
});
restate.endpoint().bind(roleUpdateService).listen();

Service handlers don't have access to Restate's K/V store.


import * as restate from "@restatedev/restate-sdk";
import { Context } from "@restatedev/restate-sdk";
export const roleUpdateService = restate.service({
name: "roleUpdate",
handlers: {
applyRoleUpdate: async (ctx: Context, update: UpdateRequest) => {
const { userId, role, permissions } = update;
const success = await ctx.run(() => applyUserRole(userId, role));
if (!success) {
return;
}
for (const permission of permissions) {
await ctx.run(() => applyPermission(userId, permission));
}
},
},
});
restate.endpoint().bind(roleUpdateService).listen();

Restate makes sure that handlers run to completion, even in the presence of failures. Restate logs the results of actions in the system. Restate takes care of retries and recovers the handler to the point where it failed.

The handlers of services are independent and can be invoked concurrently.

Handlers use the regular code and control flow, no custom DSLs.

Service handlers don't have access to Restate's K/V store.


import * as restate from "@restatedev/restate-sdk";
import { Context } from "@restatedev/restate-sdk";
export const roleUpdateService = restate.service({
name: "roleUpdate",
handlers: {
applyRoleUpdate: async (ctx: Context, update: UpdateRequest) => {
const { userId, role, permissions } = update;
const success = await ctx.run(() => applyUserRole(userId, role));
if (!success) {
return;
}
for (const permission of permissions) {
await ctx.run(() => applyPermission(userId, permission));
}
},
},
});
restate.endpoint().bind(roleUpdateService).listen();

In the example, we use a Restate service to update different systems and to make sure all updates are applied. During retries, the service will not reapply the same update twice.

Virtual objects

Virtual objects expose a set of handlers with access to K/V state stored in Restate.

A virtual object is uniquely identified and accessed by its key.


import * as restate from "@restatedev/restate-sdk";
import { ObjectContext } from "@restatedev/restate-sdk";
export const greeterObject = restate.object({
name: "greeter",
handlers: {
greet: async (ctx: ObjectContext, greeting: string) => {
let count = (await ctx.get<number>("count")) ?? 0;
count++;
ctx.set("count", count);
return `${greeting} ${ctx.key} for the ${count}-th time.`;
},
ungreet: async (ctx: ObjectContext) => {
let count = (await ctx.get<number>("count")) ?? 0;
if (count > 0) {
count--;
}
ctx.set("count", count);
return `Dear ${ctx.key}, taking one greeting back: ${count}.`;
},
},
});
restate.endpoint().bind(greeterObject).listen();

Each virtual object has access to its own isolated K/V state, stored in Restate. The handlers of a virtual object can read and write to the state of the object. Restate delivers the state together with the request to the virtual object, so virtual objects have their state locally accessible without requiring any database connection or lookup. State is exclusive, and atomically committed with the method execution.


import * as restate from "@restatedev/restate-sdk";
import { ObjectContext } from "@restatedev/restate-sdk";
export const greeterObject = restate.object({
name: "greeter",
handlers: {
greet: async (ctx: ObjectContext, greeting: string) => {
let count = (await ctx.get<number>("count")) ?? 0;
count++;
ctx.set("count", count);
return `${greeting} ${ctx.key} for the ${count}-th time.`;
},
ungreet: async (ctx: ObjectContext) => {
let count = (await ctx.get<number>("count")) ?? 0;
if (count > 0) {
count--;
}
ctx.set("count", count);
return `Dear ${ctx.key}, taking one greeting back: ${count}.`;
},
},
});
restate.endpoint().bind(greeterObject).listen();

To ensure consistent writes to the state, Restate provides concurrency guarantees: at most one handler can execute at a time for a given virtual object. This can also be used for example to implement a locking mechanism or to ensure single writer to a database row.


import * as restate from "@restatedev/restate-sdk";
import { ObjectContext } from "@restatedev/restate-sdk";
export const greeterObject = restate.object({
name: "greeter",
handlers: {
greet: async (ctx: ObjectContext, greeting: string) => {
let count = (await ctx.get<number>("count")) ?? 0;
count++;
ctx.set("count", count);
return `${greeting} ${ctx.key} for the ${count}-th time.`;
},
ungreet: async (ctx: ObjectContext) => {
let count = (await ctx.get<number>("count")) ?? 0;
if (count > 0) {
count--;
}
ctx.set("count", count);
return `Dear ${ctx.key}, taking one greeting back: ${count}.`;
},
},
});
restate.endpoint().bind(greeterObject).listen();

A virtual object is uniquely identified and accessed by its key.

Each virtual object has access to its own isolated K/V state, stored in Restate. The handlers of a virtual object can read and write to the state of the object. Restate delivers the state together with the request to the virtual object, so virtual objects have their state locally accessible without requiring any database connection or lookup. State is exclusive, and atomically committed with the method execution.

To ensure consistent writes to the state, Restate provides concurrency guarantees: at most one handler can execute at a time for a given virtual object. This can also be used for example to implement a locking mechanism or to ensure single writer to a database row.


import * as restate from "@restatedev/restate-sdk";
import { ObjectContext } from "@restatedev/restate-sdk";
export const greeterObject = restate.object({
name: "greeter",
handlers: {
greet: async (ctx: ObjectContext, greeting: string) => {
let count = (await ctx.get<number>("count")) ?? 0;
count++;
ctx.set("count", count);
return `${greeting} ${ctx.key} for the ${count}-th time.`;
},
ungreet: async (ctx: ObjectContext) => {
let count = (await ctx.get<number>("count")) ?? 0;
if (count > 0) {
count--;
}
ctx.set("count", count);
return `Dear ${ctx.key}, taking one greeting back: ${count}.`;
},
},
});
restate.endpoint().bind(greeterObject).listen();

Workflows

A workflow is a special type of Virtual Object that can be used to implement a set of steps that need to be executed durably. Workflows have additional capabilities such as signaling, querying, additional invocation options, and a longer retention time in the CLI.

A workflow has a run handler that implements the workflow logic. The run handler runs exactly once per workflow ID (object).


const payment = restate.workflow({
name: "payment",
handlers: {
run: async (ctx: restate.WorkflowContext, payment: PaymentRequest) => {
// Validate payment. If not valid, end workflow right here without retries.
if (payment.amount < 0) {
throw new restate.TerminalError("Payment refused: negative amount");
}
await ctx.run("make a payment", async () => {
await paymentClnt.charge(ctx.key, payment.account, payment.amount);
});
await ctx.promise<PaymentSuccess>("payment.success");
ctx.set("status", "Payment succeeded");
await ctx.run("notify the user", async () => {
await emailClnt.sendSuccessNotification(payment.email);
});
ctx.set("status", "User notified of payment success");
return "success";
},
paymentWebhook: async (
ctx: restate.WorkflowSharedContext,
account: string
) => {
await ctx.promise<PaymentSuccess>("payment.success").resolve({ account });
},
status: (ctx: restate.WorkflowSharedContext) => ctx.get("status"),
},
});
restate.endpoint().bind(payment).listen();

You can query the workflow by defining other handlers in the same object. For example, you can store state in the workflow object, and query it from other handlers.


const payment = restate.workflow({
name: "payment",
handlers: {
run: async (ctx: restate.WorkflowContext, payment: PaymentRequest) => {
// Validate payment. If not valid, end workflow right here without retries.
if (payment.amount < 0) {
throw new restate.TerminalError("Payment refused: negative amount");
}
await ctx.run("make a payment", async () => {
await paymentClnt.charge(ctx.key, payment.account, payment.amount);
});
await ctx.promise<PaymentSuccess>("payment.success");
ctx.set("status", "Payment succeeded");
await ctx.run("notify the user", async () => {
await emailClnt.sendSuccessNotification(payment.email);
});
ctx.set("status", "User notified of payment success");
return "success";
},
paymentWebhook: async (
ctx: restate.WorkflowSharedContext,
account: string
) => {
await ctx.promise<PaymentSuccess>("payment.success").resolve({ account });
},
status: (ctx: restate.WorkflowSharedContext) => ctx.get("status"),
},
});
restate.endpoint().bind(payment).listen();

You can signal the workflow, to send information to it, via Durable Promises. For example, the payment provider signals the workflow that the payment was successful by calling the paymentWebhook.


const payment = restate.workflow({
name: "payment",
handlers: {
run: async (ctx: restate.WorkflowContext, payment: PaymentRequest) => {
// Validate payment. If not valid, end workflow right here without retries.
if (payment.amount < 0) {
throw new restate.TerminalError("Payment refused: negative amount");
}
await ctx.run("make a payment", async () => {
await paymentClnt.charge(ctx.key, payment.account, payment.amount);
});
await ctx.promise<PaymentSuccess>("payment.success");
ctx.set("status", "Payment succeeded");
await ctx.run("notify the user", async () => {
await emailClnt.sendSuccessNotification(payment.email);
});
ctx.set("status", "User notified of payment success");
return "success";
},
paymentWebhook: async (
ctx: restate.WorkflowSharedContext,
account: string
) => {
await ctx.promise<PaymentSuccess>("payment.success").resolve({ account });
},
status: (ctx: restate.WorkflowSharedContext) => ctx.get("status"),
},
});
restate.endpoint().bind(payment).listen();

A workflow has a run handler that implements the workflow logic. The run handler runs exactly once per workflow ID (object).

You can query the workflow by defining other handlers in the same object. For example, you can store state in the workflow object, and query it from other handlers.

You can signal the workflow, to send information to it, via Durable Promises. For example, the payment provider signals the workflow that the payment was successful by calling the paymentWebhook.


const payment = restate.workflow({
name: "payment",
handlers: {
run: async (ctx: restate.WorkflowContext, payment: PaymentRequest) => {
// Validate payment. If not valid, end workflow right here without retries.
if (payment.amount < 0) {
throw new restate.TerminalError("Payment refused: negative amount");
}
await ctx.run("make a payment", async () => {
await paymentClnt.charge(ctx.key, payment.account, payment.amount);
});
await ctx.promise<PaymentSuccess>("payment.success");
ctx.set("status", "Payment succeeded");
await ctx.run("notify the user", async () => {
await emailClnt.sendSuccessNotification(payment.email);
});
ctx.set("status", "User notified of payment success");
return "success";
},
paymentWebhook: async (
ctx: restate.WorkflowSharedContext,
account: string
) => {
await ctx.promise<PaymentSuccess>("payment.success").resolve({ account });
},
status: (ctx: restate.WorkflowSharedContext) => ctx.get("status"),
},
});
restate.endpoint().bind(payment).listen();

note

The run handler is the only handler that can write K/V state. The other handlers are able to run concurrently to the run handler, and can get state but cannot set it.

Restate Server

In between the services, sits the Restate Server. It proxies invocations to the services and manages their lifecycle.

The Restate Server is written in Rust, to be self-contained and resource-efficient. It has an event-driven foundation. You can put it in the hot, latency-sensitive paths of your applications.

The main feature the Restate Server provides is Durable Execution. We dive into this in a later section.

The Restate Server runs as a single binary with zero dependencies. It runs with low operational overhead on any platform, also locally. To deploy the Restate Server, have a look at these deployment guides: