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 (currently TypeScript and Java/Kotlin).
There are two types of services in Restate:
Services (plain) | Virtual objects |
---|---|
Set of handlers durably executed | Set of handlers durably executed |
No associated K/V store | Handlers share K/V state; isolated per virtual object |
No concurrency limits | Single concurrent invocation per virtual object |
Example use cases:
| Example use cases:
|
Services
Services expose a collection of handlers:
- TypeScript
- Java
- 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 { @Handler public void applyRoleUpdate(Context ctx, UpdateRequest req) { boolean success = ctx.run(CoreSerdes.JSON_BOOLEAN, () -> SystemA.applyUserRole(req.getUserId(), req.getRole())); if (!success) { return; } for(String permission: req.getPermissions()) { ctx.run(CoreSerdes.JSON_BOOLEAN, () -> SystemB.applyPermission(req.getUserId(), permission)); } } public static void main(String[] args) { RestateHttpEndpointBuilder.builder() .bind(new RoleUpdateService()) .buildAndListen(); }}
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
- 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 final static StateKey<Integer> COUNT = StateKey.of("count", CoreSerdes.JSON_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(); }}
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: