Services
This is what a Restate application looks like from a helicopter view:
- Restate Server: The server intercepts incoming requests and drives their execution till the end.
- Services: Contain the handlers which process incoming requests.
- 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 objects | Workflows |
---|---|---|
Set of handlers durably executed | Set of handlers durably executed | The workflow run handler is durably executed a single time. |
No concurrency limits | Single concurrent invocation per virtual object | The run handler can run only a single time. Other handlers can run concurrently to interact with the workflow. |
No associated K/V store | Handlers share K/V state; isolated per virtual object | K/V state isolated per workflow execution. Can only be set by the run handler. |
Example use cases:
| Example use cases:
| Example use cases:
|
Services
Services expose a collection of handlers:
- TypeScript
- Java
- Go
- Python
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();
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.
@Servicepublic class RoleUpdateService {@Handlerpublic void applyRoleUpdate(Context ctx, UpdateRequest req) {boolean success =ctx.run(JsonSerdes.BOOLEAN,() -> SystemA.applyUserRole(req.getUserId(), req.getRole()));if (!success) {return;}for (Permission permission : req.getPermissions()) {ctx.run(JsonSerdes.BOOLEAN,() -> SystemB.applyPermission(req.getUserId(), permission));}}public static void main(String[] args) {RestateHttpEndpointBuilder.builder().bind(new RoleUpdateService()).buildAndListen();}}
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.
func (RoleUpdate) ApplyRoleUpdate(ctx restate.Context, update UpdateRequest) error {success, err := restate.Run(ctx, func(ctx restate.RunContext) (bool, error) {return applyUserRole(update.UserId, update.Role)})if err != nil {return err}if !success {return nil}for _, permission := range update.Permissions {if _, err := restate.Run(ctx, func(ctx restate.RunContext) (restate.Void, error) {return restate.Void{}, applyPermission(update.UserId, permission)}); err != nil {return err}}return nil}func main() {if err := server.NewRestate().Bind(restate.Reflect(RoleUpdate{})).Start(context.Background(), ":9080"); err != nil {log.Fatal(err)}}
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.
role_update_service = Service("RoleUpdateService")@role_update_service.handler()async def apply_role_update(ctx: Context, update: UpdateRequest):success = await ctx.run("role",lambda: apply_user_role(update["userId"], update["role"]))if not success:returnfor permission in update["permissions"]:await ctx.run("permission",lambda: apply_permission(update["userId"], permission))app = restate.app([role_update_service])
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.
- TypeScript
- Java
- Go
- Python
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();
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.
Concurrency guarantees: to ensure consistent writes to the state, 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.
@VirtualObjectpublic class Greeter {public static final StateKey<Integer> COUNT =StateKey.of("count", JsonSerdes.INT);@Handlerpublic String greet(ObjectContext ctx, String greeting) {Integer count = ctx.get(COUNT).orElse(0);count++;ctx.set(COUNT, count);return greeting + " " + ctx.key() + "for the " + count + "-th time";}@Handlerpublic String ungreet(ObjectContext ctx) {Integer count = ctx.get(COUNT).orElse(0);if (count > 0) {count--;}ctx.set(COUNT, count);return "Dear " + ctx.key() + ", taking one greeting back";}public static void main(String[] args) {RestateHttpEndpointBuilder.builder().bind(new Greeter()).buildAndListen();}}
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.
func Greet(ctx restate.ObjectContext, greeting string) (string, error) {count, err := restate.Get[int](ctx, "count")if err != nil {return "", err}count++restate.Set(ctx, "count", count)return fmt.Sprintf("%s %s for the %d-th time.",greeting, restate.Key(ctx), count,), nil}func Ungreet(ctx restate.ObjectContext) (string, error) {count, err := restate.Get[int](ctx, "count")if err != nil {return "", err}if count > 0 {count--}restate.Set(ctx, "count", count)return fmt.Sprintf("Dear %s, taking one greeting back: %d.",restate.Key(ctx), count,), nil}func main() {if err := server.NewRestate().Bind(restate.Reflect(Greeter{})).Start(context.Background(), ":9080"); err != nil {log.Fatal(err)}}
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.
greeter = VirtualObject("Greeter")@greeter.handler()async def greet(ctx: ObjectContext, greeting: str) -> str:count = await ctx.get("count") or 0count += 1ctx.set("count", count)return f"{greeting} {ctx.key} for the {count}-th time."@greeter.handler()async def ungreet(ctx: ObjectContext) -> str:count = await ctx.get("count") or 0if count > 0:count -= 1ctx.set("count", count)return f"Dear {ctx.key}, taking one greeting back: {count}."app = restate.app([greeter])
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.
- TypeScript
- Java
- Python
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();
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
.
@Workflowpublic class Payment {private static final StateKey<String> STATUS =StateKey.of("status", JsonSerdes.STRING);private static final DurablePromiseKey<PaymentSuccess> PAYMENT_SUCCESS =DurablePromiseKey.of("success", JacksonSerdes.of(PaymentSuccess.class));@Workflowpublic String run(WorkflowContext ctx, PaymentRequest req) {if (req.getAmount() < 0) {throw new TerminalException("Payment refused: negative amount");}ctx.run("make a req",JsonSerdes.BOOLEAN,() -> PaymentClient.charge(req.getAccount(), req.getAmount()));ctx.promise(PAYMENT_SUCCESS).awaitable().await();ctx.set(STATUS, "Payment succeeded");ctx.run("notify the user",JsonSerdes.BOOLEAN,() -> EmailClient.sendSuccessNotification(req.getEmail()));ctx.set(STATUS, "User notified of req success");return "success";}@Sharedpublic void paymentWebhook(SharedWorkflowContext ctx, PaymentSuccess msg) {ctx.promiseHandle(PAYMENT_SUCCESS).resolve(msg);}@Sharedpublic String getStatus(SharedWorkflowContext ctx) {return ctx.get(STATUS).orElse("unknown");}public static void main(String[] args) {RestateHttpEndpointBuilder.builder().bind(new Payment()).buildAndListen();}}
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
.
payment_workflow = Workflow("Payment")@payment_workflow.main()async def run(ctx: WorkflowContext, payment: PaymentRequest):# Validate payment. If not valid, end workflow right here without retries.if payment["amount"] < 0:raise TerminalError("Payment refused: negative amount")async def pay():return await payment_client.charge(ctx.key(), payment["account"], payment["amount"])await ctx.run("make a payment", pay)await ctx.promise("payment.success").value()ctx.set("status", "Payment succeeded")async def email():return await email_client.send_success_notification(payment["email"])await ctx.run("notify the user", email)ctx.set("status", "User notified of payment success")return "success"@payment_workflow.handler()async def payment_webhook(ctx: WorkflowSharedContext, account: str):await ctx.promise("payment.success").resolve(account)@payment_workflow.handler()async def status(ctx: WorkflowSharedContext):await ctx.get("status")app = restate.app([payment_workflow])
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: