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.
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();
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.
@Servicepublic class RoleUpdateService { @Handler public 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(); }}
The handlers of services are independent and can be invoked concurrently.
@Servicepublic class RoleUpdateService { @Handler public 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(); }}
Handlers use the regular code and control flow, no custom DSLs.
@Servicepublic class RoleUpdateService { @Handler public 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(); }}
Service handlers don't have access to Restate's K/V store.
@Servicepublic class RoleUpdateService { @Handler public 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.
@Servicepublic class RoleUpdateService { @Handler public 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.
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) }}
The handlers of services are independent and can be invoked concurrently.
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) }}
Handlers use the regular code and control flow, no custom DSLs.
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) }}
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.
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.
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: return for permission in update["permissions"]: await ctx.run("permission", lambda: apply_permission(update["userId"], permission))app = restate.app([role_update_service])
The handlers of services are independent and can be invoked concurrently.
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: return for permission in update["permissions"]: await ctx.run("permission", lambda: apply_permission(update["userId"], permission))app = restate.app([role_update_service])
Handlers use the regular code and control flow, no custom DSLs.
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: return for permission in update["permissions"]: await ctx.run("permission", lambda: apply_permission(update["userId"], permission))app = restate.app([role_update_service])
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: return for permission in update["permissions"]: await ctx.run("permission", lambda: apply_permission(update["userId"], permission))app = restate.app([role_update_service])
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: return for 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.
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();
A virtual object is uniquely identified and accessed by its key.
@VirtualObjectpublic class Greeter { public static final StateKey<Integer> COUNT = StateKey.of("count", JsonSerdes.INT); @Handler public 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"; } @Handler public 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(); }}
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.
@VirtualObjectpublic class Greeter { public static final StateKey<Integer> COUNT = StateKey.of("count", JsonSerdes.INT); @Handler public 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"; } @Handler public 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(); }}
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); @Handler public 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"; } @Handler public 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.
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); @Handler public 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"; } @Handler public 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.
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) }}
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.
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) }}
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.
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.
greeter = VirtualObject("Greeter")@greeter.handler()async def greet(ctx: ObjectContext, greeting: str) -> str: count = await ctx.get("count") or 0 count += 1 ctx.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 0 if count > 0: count -= 1 ctx.set("count", count) return f"Dear {ctx.key}, taking one greeting back: {count}."app = restate.app([greeter])
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.
greeter = VirtualObject("Greeter")@greeter.handler()async def greet(ctx: ObjectContext, greeting: str) -> str: count = await ctx.get("count") or 0 count += 1 ctx.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 0 if count > 0: count -= 1 ctx.set("count", count) return f"Dear {ctx.key}, taking one greeting back: {count}."app = restate.app([greeter])
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 0 count += 1 ctx.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 0 if count > 0: count -= 1 ctx.set("count", count) return f"Dear {ctx.key}, taking one greeting back: {count}."app = restate.app([greeter])
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 0 count += 1 ctx.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 0 if count > 0: count -= 1 ctx.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).
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();
A workflow has a run
handler that implements the workflow logic.
The run
handler runs exactly once per workflow ID (object).
@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)); @Workflow public 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"; } @Shared public void paymentWebhook(SharedWorkflowContext ctx, PaymentSuccess msg) { ctx.promiseHandle(PAYMENT_SUCCESS).resolve(msg); } @Shared public String getStatus(SharedWorkflowContext ctx) { return ctx.get(STATUS).orElse("unknown"); } public static void main(String[] args) { RestateHttpEndpointBuilder.builder() .bind(new Payment()) .buildAndListen(); }}
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.
@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)); @Workflow public 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"; } @Shared public void paymentWebhook(SharedWorkflowContext ctx, PaymentSuccess msg) { ctx.promiseHandle(PAYMENT_SUCCESS).resolve(msg); } @Shared public String getStatus(SharedWorkflowContext ctx) { return ctx.get(STATUS).orElse("unknown"); } public static void main(String[] args) { RestateHttpEndpointBuilder.builder() .bind(new Payment()) .buildAndListen(); }}
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)); @Workflow public 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"; } @Shared public void paymentWebhook(SharedWorkflowContext ctx, PaymentSuccess msg) { ctx.promiseHandle(PAYMENT_SUCCESS).resolve(msg); } @Shared public 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
.
@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)); @Workflow public 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"; } @Shared public void paymentWebhook(SharedWorkflowContext ctx, PaymentSuccess msg) { ctx.promiseHandle(PAYMENT_SUCCESS).resolve(msg); } @Shared public 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).
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])
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.
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])
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])
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: