Services
This is what a Restate application looks like from a helicopter view:
- Services: Restate services contain functions/handlers which process incoming requests and execute business logic. Services run like regular code in your infrastructure, for example a NodeJS/Java app in a Docker container or a Python function on AWS Lambda. Services embed the Restate SDK as a dependency, and their handlers use it to persist the progress they make. Services can be written in any language for which there is an SDK available: TypeScript, Java, Kotlin, Go, Python, and Rust.
- Restate Server: The server sits in front of your services, similar to a reverse proxy or message broker. It proxies incoming requests to the corresponding services and drives their execution till the end.
- Invocation: An invocation is a request to execute a handler.
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 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. |
No concurrency limits; unlimited scale-out on platforms like AWS Lambda. |
| The run handler can run only a single time per workflow ID. Other handlers can run concurrently to interact with the workflow. |
Example use cases:
| Example use cases:
| Example use cases:
|
Services
Services expose a collection of handlers:
Restate makes sure that handlers run to completion, even in the presence of failures. Restate persists the results of actions and recovers them after failures.
Restate makes sure that handlers run to completion, even in the presence of failures. Restate persists the results of actions and recovers them after failures.
The handlers of services are independent and can be invoked concurrently.
The handlers of services are independent and can be invoked concurrently.
Handlers use regular code and control flow, no custom DSLs.
Handlers use regular code and control flow, no custom DSLs.
Handlers are exposed over HTTP. When the Restate Server receives a request, it sets up an HTTP2 connection with the service and streams events back and forth over this connection.
Alternatively, you can create a serverless function, like an AWS Lambda handler.
Handlers are exposed over HTTP. When the Restate Server receives a request, it sets up an HTTP2 connection with the service and streams events back and forth over this connection.
Alternatively, you can create a serverless function, like an AWS Lambda handler.
- TypeScript
- Java
- Kotlin
- Go
- Python
- Rust
const subscriptionService = restate.service({name: "SubscriptionService",handlers: {add: async (ctx: restate.Context, req: SubscriptionRequest) => {const paymentId = ctx.rand.uuidv4();const payRef = await ctx.run(() =>createRecurringPayment(req.creditCard, paymentId));for (const subscription of req.subscriptions) {await ctx.run(() =>createSubscription(req.userId, subscription, payRef));}},},});restate.endpoint().bind(subscriptionService).listen(9080);
@Servicepublic class SubscriptionService {@Handlerpublic void add(Context ctx, SubscriptionRequest req) {var paymentId = ctx.random().nextUUID().toString();var payRef =ctx.run(String.class,() -> createRecurringPayment(req.creditCard(), paymentId));for (String subscription : req.subscriptions()) {ctx.run(() -> createSubscription(req.userId(), subscription, payRef));}}public static void main(String[] args) {RestateHttpServer.listen(Endpoint.bind(new SubscriptionService()));}}
@Serviceclass SubscriptionService {@Handlersuspend fun add(ctx: Context, req: SubscriptionRequest) {val paymentId = ctx.random().nextUUID().toString()val payRef = ctx.runBlock { createRecurringPayment(req.creditCard, paymentId) }for (subscription in req.subscriptions) {ctx.runBlock { createSubscription(req.userId, subscription, payRef) }}}}fun main() {RestateHttpServer.listen(endpoint { bind(SubscriptionService()) })}
type SubscriptionService struct{}func (SubscriptionService) Add(ctx restate.Context, req SubscriptionRequest) error {paymentId := restate.Rand(ctx).UUID().String()payRef, err := restate.Run(ctx, func(ctx restate.RunContext) (string, error) {return CreateRecurringPayment(req.CreditCard, paymentId)})if err != nil {return err}for _, subscription := range req.Subscriptions {if _, err := restate.Run(ctx, func(ctx restate.RunContext) (restate.Void, error) {return restate.Void{}, CreateSubscription(req.UserID, subscription, payRef)}); err != nil {return err}}return nil}func main() {if err := server.NewRestate().Bind(restate.Reflect(SubscriptionService{})).Start(context.Background(), ":9080"); err != nil {log.Fatal(err)}}
subscription_service = Service("SubscriptionService")@subscription_service.handler()async def add(ctx: Context, req: SubscriptionRequest):payment_id = await ctx.run("payment id", lambda: str(uuid.uuid4()))pay_ref = await ctx.run("recurring payment",lambda: create_recurring_payment(req.credit_card, payment_id),)for subscription in req.subscriptions:await ctx.run("subscription",lambda: create_subscription(req.user_id, subscription, pay_ref),)app = restate.app([subscription_service])
#[restate_sdk::service]pub trait SubscriptionService {async fn add(req: Json<SubscriptionRequest>) -> Result<(), HandlerError>;}struct SubscriptionServiceImpl;impl SubscriptionService for SubscriptionServiceImpl {async fn add(&self,mut ctx: Context<'_>,Json(req): Json<SubscriptionRequest>,) -> Result<(), HandlerError> {let payment_id = ctx.rand_uuid().to_string();let pay_ref = ctx.run(|| create_recurring_payment(&req.credit_card, &payment_id)).await?;for subscription in req.subscriptions {ctx.run(|| create_subscription(&req.user_id, &subscription, &pay_ref)).await?;}Ok(())}}#[tokio::main]async fn main() {HttpServer::new(Endpoint::builder().bind(SubscriptionServiceImpl.serve()).build(),).listen_and_serve("0.0.0.0:9080".parse().unwrap()).await;}
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.
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 handler execution.
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 handler execution.
When a handler is invoked, it can read and write to the state of the virtual object. 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.
When a handler is invoked, it can read and write to the state of the virtual object. 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.
If you want to allow concurrent reads to the state, you can mark a handler as a shared handler. This allows the handler to run concurrently with other handlers, but it cannot write to the state.
If you want to allow concurrent reads to the state, you can mark a handler as a shared handler. This allows the handler to run concurrently with other handlers, but it cannot write to the state.
- TypeScript
- Java
- Kotlin
- Go
- Python
- Rust
export const greeterObject = restate.object({name: "greeter",handlers: {greet: async (ctx: restate.ObjectContext, req: { greeting: string }) => {const name = ctx.key;let count = (await ctx.get<number>("count")) ?? 0;count++;ctx.set("count", count);return `${req.greeting} ${name} for the ${count}-th time.`;},ungreet: async (ctx: restate.ObjectContext) => {const name = ctx.key;let count = (await ctx.get<number>("count")) ?? 0;if (count > 0) {count--;}ctx.set("count", count);return `Dear ${name}, taking one greeting back: ${count}.`;},getGreetCount: handlers.object.shared(async (ctx: restate.ObjectSharedContext) => {return await ctx.get<number>("count") ?? 0;}),},});restate.endpoint().bind(greeterObject).listen();
@VirtualObjectpublic class Greeter {public static final StateKey<Integer> COUNT = StateKey.of("count", Integer.class);@Handlerpublic String greet(ObjectContext ctx, String greeting) {String name = ctx.key();int count = ctx.get(COUNT).orElse(0);int newCount = count + 1;ctx.set(COUNT, newCount);return String.format("%s %s, for the %d-th time", greeting, name, newCount);}@Handlerpublic String ungreet(ObjectContext ctx) {String name = ctx.key();int count = ctx.get(COUNT).orElse(0);if (count > 0) {int newCount = count - 1;ctx.set(COUNT, newCount);}return String.format("Dear %s, taking one greeting back: %d", name, count);}@Sharedpublic int getGreetCount(SharedObjectContext ctx) {return ctx.get(COUNT).orElse(0);}public static void main(String[] args) {RestateHttpServer.listen(Endpoint.bind(new Greeter()));}}
@VirtualObjectclass GreeterObject {companion object {private val COUNT = stateKey<Int>("greet-count")}@Handlersuspend fun greet(ctx: ObjectContext, greeting: String): String {val name = ctx.key()val count = ctx.get(COUNT) ?: 0val newCount = count + 1ctx.set(COUNT, newCount)return "$greeting ${name}, for the $newCount-th time"}@Handlersuspend fun ungreet(ctx: ObjectContext): String {val name = ctx.key()val count = ctx.get(COUNT) ?: 0if (count > 0) {val newCount = count - 1ctx.set(COUNT, newCount)}return "Dear ${name}, taking one greeting back: $count"}@Sharedsuspend fun getGreetCount(ctx: SharedObjectContext): Int {return ctx.get(COUNT) ?: 0}}fun main() {RestateHttpServer.listen(endpoint { bind(GreeterObject()) })}
type Greeter struct{}func Greet(ctx restate.ObjectContext, greeting string) (string, error) {name := restate.Key(ctx)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, name, count,), nil}func Ungreet(ctx restate.ObjectContext) (string, error) {name := restate.Key(ctx)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.",name, count,), nil}func GetGreetCount(ctx restate.ObjectSharedContext) (int, error) {return restate.Get[int](ctx, "count")}func main() {if err := server.NewRestate().Bind(restate.Reflect(Greeter{})).Start(context.Background(), ":9080"); err != nil {log.Fatal(err)}}
greeter = VirtualObject("Greeter")@greeter.handler()async def greet(ctx: restate.ObjectContext, greeting: str) -> str:name = ctx.keycount = await ctx.get("count") or 0count += 1ctx.set("count", count)return f"{greeting} {name} for the {count}-th time."@greeter.handler()async def ungreet(ctx: restate.ObjectContext) -> str:name = ctx.keycount = await ctx.get("count") or 0if count > 0:count -= 1ctx.set("count", count)return f"Dear {name}, taking one greeting back: {count}."@greeter.handler(kind="shared")async def get_greet_count(ctx: restate.ObjectSharedContext) -> int:return await ctx.get("count") or 0app = restate.app([greeter])
#[restate_sdk::object]pub trait GreeterObject {async fn greet(req: String) -> Result<String, HandlerError>;async fn ungreet() -> Result<String, HandlerError>;#[shared]async fn get_greet_count() -> Result<u64, HandlerError>;}pub struct GreeterObjectImpl;impl GreeterObject for GreeterObjectImpl {async fn greet(&self,ctx: ObjectContext<'_>,greeting: String,) -> Result<String, HandlerError> {let name = ctx.key();let mut count = ctx.get::<u64>("count").await?.unwrap_or(0);count += 1;ctx.set("count", count);Ok(format!("{} {} for the {}-th time.", greeting, name, count))}async fn ungreet(&self, ctx: ObjectContext<'_>) -> Result<String, HandlerError> {let name = ctx.key();let mut count: u64 = ctx.get::<u64>("count").await?.unwrap_or(0);if count > 0 {count -= 1;}ctx.set("count", count);Ok(format!("Dear {}, taking one greeting back: {}.",name, count))}async fn get_greet_count(&self, ctx: SharedObjectContext) -> Result<u64, HandlerError> {Ok(ctx.get::<u64>("count").await?.unwrap_or(0))}}#[tokio::main]async fn main() {HttpServer::new(Endpoint::builder().bind(GreeterObjectImpl.serve()).build()).listen_and_serve("0.0.0.0:9080".parse().unwrap()).await;}
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).
A workflow has a run handler that implements the workflow logic.
The run
handler runs exactly once per workflow ID (object).
The run handler executes a set of durable steps/activities. These can either be:
- Inline activities: run blocks, sleep, mutating K/V state,...
- Calls to other handlers implementing the activities.
The run handler executes a set of durable steps/activities. These can either be:
- Inline activities: run blocks, sleep, mutating K/V state,...
- Calls to other handlers implementing the activities.
You can define other handlers in the same workflow that can run concurrently to the run
handler and:
- Query the workflow (get information out of it) by getting K/V state or awaiting promises that are resolved by the workflow.
- Signal the workflow (send information to it) by resolving promises that the workflow waits on. For example, the click handler signals the workflow that the email link was clicked.
You can define other handlers in the same workflow that can run concurrently to the run
handler and:
- Query the workflow (get information out of it) by getting K/V state or awaiting promises that are resolved by the workflow.
- Signal the workflow (send information to it) by resolving promises that the workflow waits on. For example, the click handler signals the workflow that the email link was clicked.
- TypeScript
- Java
- Kotlin
- Go
- Python
- Rust
const signupWorkflow = restate.workflow({name: "user-signup",handlers: {run: async (ctx: restate.WorkflowContext,user: { name: string; email: string }) => {// workflow ID = user ID; workflow runs once per userconst userId = ctx.key;await ctx.run(() => createUserEntry(user));const secret = ctx.rand.uuidv4();await ctx.run(() => sendEmailWithLink({ userId, user, secret }));const clickSecret = await ctx.promise<string>("link-clicked");return clickSecret === secret;},click: async (ctx: restate.WorkflowSharedContext,request: { secret: string }) => {await ctx.promise<string>("link-clicked").resolve(request.secret);},},});restate.endpoint().bind(signupWorkflow).listen(9080);
@Workflowpublic class SignupWorkflow {private static final DurablePromiseKey<String> LINK_CLICKED =DurablePromiseKey.of("link_clicked", String.class);@Workflowpublic boolean run(WorkflowContext ctx, User user) {// workflow ID = user ID; workflow runs once per userString userId = ctx.key();ctx.run(() -> createUserEntry(user));String secret = ctx.random().nextUUID().toString();ctx.run(() -> sendEmailWithLink(userId, user, secret));String clickSecret = ctx.promise(LINK_CLICKED).future().await();return clickSecret.equals(secret);}@Sharedpublic void click(SharedWorkflowContext ctx, String secret) {ctx.promiseHandle(LINK_CLICKED).resolve(secret);}public static void main(String[] args) {RestateHttpServer.listen(Endpoint.bind(new SignupWorkflow()));}}
@Workflowclass SignupWorkflow {companion object {private val LINK_CLICKED = durablePromiseKey<String>("link_clicked")}@Workflowsuspend fun run(ctx: WorkflowContext, user: User): Boolean {// workflow ID = user ID; workflow runs once per userval userId = ctx.key()ctx.runBlock { createUserEntry(user) }val secret = ctx.random().nextUUID().toString()ctx.runBlock { sendEmailWithLink(userId, user, secret) }val clickSecret: String = ctx.promise(LINK_CLICKED).future().await()return clickSecret == secret}@Sharedsuspend fun click(ctx: SharedWorkflowContext, secret: String) {ctx.promiseHandle(LINK_CLICKED).resolve(secret)}}fun main() {RestateHttpServer.listen(endpoint { bind(SignupWorkflow()) })}
type SignupWorkflow struct{}func (SignupWorkflow) Run(ctx restate.WorkflowContext, user User) (bool, error) {// workflow ID = user ID; workflow runs once per useruserId := restate.Key(ctx)if _, err := restate.Run(ctx, func(ctx restate.RunContext) (restate.Void, error) {return restate.Void{}, CreateUserEntry(user)}); err != nil {return false, err}secret := restate.Rand(ctx).UUID().String()if _, err := restate.Run(ctx, func(ctx restate.RunContext) (restate.Void, error) {return restate.Void{}, SendEmailWithLink(userId, user, secret)}); err != nil {return false, err}clickSecret, err := restate.Promise[string](ctx, "link-clicked").Result()if err != nil {return false, err}return clickSecret == secret, nil}func (SignupWorkflow) Click(ctx restate.WorkflowSharedContext, secret string) error {return restate.Promise[string](ctx, "link-clicked").Resolve(secret)}func main() {if err := server.NewRestate().Bind(restate.Reflect(SignupWorkflow{})).Start(context.Background(), ":9080"); err != nil {log.Fatal(err)}}
user_signup = Workflow("user-signup")@user_signup.main()async def run(ctx: WorkflowContext, user: User) -> bool:# workflow ID = user ID; workflow runs once per useruser_id = ctx.key()await ctx.run("create_user", lambda: create_user_entry(user))secret = await ctx.run("secret", lambda: str(uuid.uuid4()))await ctx.run("send_email", lambda: send_email_with_link(user_id, user.email, secret))click_secret = await ctx.promise("link_clicked").value()return click_secret == secret@user_signup.handler()async def click(ctx: WorkflowSharedContext, secret: str):await ctx.promise("link_clicked").resolve(secret)app = restate.app(services=[user_signup])
#[restate_sdk::workflow]trait SignupWorkflow {async fn run(user: Json<User>) -> Result<bool, HandlerError>;#[shared]async fn click(secret: String) -> Result<(), HandlerError>;}struct SignupWorkflowImpl;impl SignupWorkflow for SignupWorkflowImpl {async fn run(&self,mut ctx: WorkflowContext<'_>,Json(user): Json<User>,) -> Result<bool, HandlerError> {// workflow ID = user ID; workflow runs once per userlet user_id = ctx.key();ctx.run(|| create_user_entry(&user)).await?;let secret = ctx.rand_uuid().to_string();ctx.run(|| send_email_with_link(user_id, &user, &secret)).await?;let click_secret = ctx.promise::<String>("email-link").await?;Ok(click_secret == secret)}async fn click(&self,ctx: SharedWorkflowContext<'_>,secret: String,) -> Result<(), HandlerError> {ctx.resolve_promise::<String>("email-link", secret);Ok(())}}#[tokio::main]async fn main() {HttpServer::new(Endpoint::builder().bind(SignupWorkflowImpl.serve()).build()).listen_and_serve("0.0.0.0:9080".parse().unwrap()).await;}
Restate Server
The Restate Server sits like reverse-proxy or message broker in front of your services and proxies invocations to them.
The Restate Server is written in Rust, to be self-contained and resource-efficient. It has an event-driven foundation to suit low-latency requirements.
The Restate Server runs as a single binary with zero dependencies. It runs with low operational overhead on any platform, also locally. You can run the Restate Server in a highly-available configuration, with multiple instances behind a load balancer.
Restate is also available as a fully managed cloud service, if all you want is to use it and let us operate it. Contact our team for more information.
Learn more about the Restate Server:
- Deploying Restate Servers and clusters
- Restate's architecture
- Deep-dive architecture blog post with benchmark results.
- Durable Execution: the main feature the Restate Server implements and how it works