Skip to main content

Durable Building Blocks

💡Context

Distributed systems are inherently complex and failures are inevitable. Almost any application is a distributed system, since they are composed of different components that communicate over the network (e.g. services, databases, queues, etc). With every component, the number of possible failure scenarios increases: network partitions, hardware failures, timeouts, race conditions etc. Building reliable applications is a challenging task.

Restate lets you write distributed applications that are resilient to failures. It does this by providing a distributed, durable version of common building blocks.

For these building blocks, Restate handles failure recovery, idempotency, state, and consistency. This way, you can implement otherwise tricky patterns in a few lines of code without worrying about these concerns.

Restate lets you implement your business logic in handlers. These handlers have access to these building blocks via the Restate SDK, that is loaded as a dependency.

Let's have a look at a handler that processes food orders:

Durable functions

Handlers take part in durable execution, meaning that Restate keeps track of their progress and recovers them to the previously reached state in case of failures.

order_processor.ts

async function process(ctx: ObjectContext, order: Order) {
// 1. Set status
ctx.set("status", Status.CREATED);
// 2. Handle payment
const token = ctx.rand.uuidv4();
const paid = await ctx.run(() =>
paymentClnt.charge(order.id, token, order.totalCost)
);
if (!paid) {
ctx.set("status", Status.REJECTED);
return;
}
// 3. Wait until the requested preparation time
ctx.set("status", Status.SCHEDULED);
await ctx.sleep(order.deliveryDelay);
// 4. Trigger preparation
const preparationPromise = ctx.awakeable();
await ctx.run(() => restaurant.prepare(order.id, preparationPromise.id));
ctx.set("status", Status.IN_PREPARATION);
await preparationPromise.promise;
ctx.set("status", Status.SCHEDULING_DELIVERY);
// 5. Find a driver and start delivery
await ctx.objectClient(deliveryManager, order.id).startDelivery(order);
ctx.set("status", Status.DELIVERED);
}

Durable RPCs and queues

Handlers can call other handlers in a resilient way, with or without waiting for the response. When a failure happens, Restate handles retries and recovers partial progress.

order_processor.ts

async function process(ctx: ObjectContext, order: Order) {
// 1. Set status
ctx.set("status", Status.CREATED);
// 2. Handle payment
const token = ctx.rand.uuidv4();
const paid = await ctx.run(() =>
paymentClnt.charge(order.id, token, order.totalCost)
);
if (!paid) {
ctx.set("status", Status.REJECTED);
return;
}
// 3. Wait until the requested preparation time
ctx.set("status", Status.SCHEDULED);
await ctx.sleep(order.deliveryDelay);
// 4. Trigger preparation
const preparationPromise = ctx.awakeable();
await ctx.run(() => restaurant.prepare(order.id, preparationPromise.id));
ctx.set("status", Status.IN_PREPARATION);
await preparationPromise.promise;
ctx.set("status", Status.SCHEDULING_DELIVERY);
// 5. Find a driver and start delivery
await ctx.objectClient(deliveryManager, order.id).startDelivery(order);
ctx.set("status", Status.DELIVERED);
}

Durable promises and timers

Register promises in Restate to make them resilient to failures (e.g. webhooks, timers). Restate lets the handler suspend while awaiting the promise, and invokes it again when the result is available. A great match for function-as-a-service platforms.

order_processor.ts

async function process(ctx: ObjectContext, order: Order) {
// 1. Set status
ctx.set("status", Status.CREATED);
// 2. Handle payment
const token = ctx.rand.uuidv4();
const paid = await ctx.run(() =>
paymentClnt.charge(order.id, token, order.totalCost)
);
if (!paid) {
ctx.set("status", Status.REJECTED);
return;
}
// 3. Wait until the requested preparation time
ctx.set("status", Status.SCHEDULED);
await ctx.sleep(order.deliveryDelay);
// 4. Trigger preparation
const preparationPromise = ctx.awakeable();
await ctx.run(() => restaurant.prepare(order.id, preparationPromise.id));
ctx.set("status", Status.IN_PREPARATION);
await preparationPromise.promise;
ctx.set("status", Status.SCHEDULING_DELIVERY);
// 5. Find a driver and start delivery
await ctx.objectClient(deliveryManager, order.id).startDelivery(order);
ctx.set("status", Status.DELIVERED);
}

Consistent K/V state

Persist application state in Restate with a simple concurrency model and no extra setup. Restate makes sure state remains consistent amid failures.

order_processor.ts

async function process(ctx: ObjectContext, order: Order) {
// 1. Set status
ctx.set("status", Status.CREATED);
// 2. Handle payment
const token = ctx.rand.uuidv4();
const paid = await ctx.run(() =>
paymentClnt.charge(order.id, token, order.totalCost)
);
if (!paid) {
ctx.set("status", Status.REJECTED);
return;
}
// 3. Wait until the requested preparation time
ctx.set("status", Status.SCHEDULED);
await ctx.sleep(order.deliveryDelay);
// 4. Trigger preparation
const preparationPromise = ctx.awakeable();
await ctx.run(() => restaurant.prepare(order.id, preparationPromise.id));
ctx.set("status", Status.IN_PREPARATION);
await preparationPromise.promise;
ctx.set("status", Status.SCHEDULING_DELIVERY);
// 5. Find a driver and start delivery
await ctx.objectClient(deliveryManager, order.id).startDelivery(order);
ctx.set("status", Status.DELIVERED);
}

Journaling actions

Store the result of an action in Restate. The result gets replayed in case of failures and the action is not executed again.

order_processor.ts

async function process(ctx: ObjectContext, order: Order) {
// 1. Set status
ctx.set("status", Status.CREATED);
// 2. Handle payment
const token = ctx.rand.uuidv4();
const paid = await ctx.run(() =>
paymentClnt.charge(order.id, token, order.totalCost)
);
if (!paid) {
ctx.set("status", Status.REJECTED);
return;
}
// 3. Wait until the requested preparation time
ctx.set("status", Status.SCHEDULED);
await ctx.sleep(order.deliveryDelay);
// 4. Trigger preparation
const preparationPromise = ctx.awakeable();
await ctx.run(() => restaurant.prepare(order.id, preparationPromise.id));
ctx.set("status", Status.IN_PREPARATION);
await preparationPromise.promise;
ctx.set("status", Status.SCHEDULING_DELIVERY);
// 5. Find a driver and start delivery
await ctx.objectClient(deliveryManager, order.id).startDelivery(order);
ctx.set("status", Status.DELIVERED);
}

Durable functions

Handlers take part in durable execution, meaning that Restate keeps track of their progress and recovers them to the previously reached state in case of failures.

Durable RPCs and queues

Handlers can call other handlers in a resilient way, with or without waiting for the response. When a failure happens, Restate handles retries and recovers partial progress.

Durable promises and timers

Register promises in Restate to make them resilient to failures (e.g. webhooks, timers). Restate lets the handler suspend while awaiting the promise, and invokes it again when the result is available. A great match for function-as-a-service platforms.

Consistent K/V state

Persist application state in Restate with a simple concurrency model and no extra setup. Restate makes sure state remains consistent amid failures.

Journaling actions

Store the result of an action in Restate. The result gets replayed in case of failures and the action is not executed again.

order_processor.ts

async function process(ctx: ObjectContext, order: Order) {
// 1. Set status
ctx.set("status", Status.CREATED);
// 2. Handle payment
const token = ctx.rand.uuidv4();
const paid = await ctx.run(() =>
paymentClnt.charge(order.id, token, order.totalCost)
);
if (!paid) {
ctx.set("status", Status.REJECTED);
return;
}
// 3. Wait until the requested preparation time
ctx.set("status", Status.SCHEDULED);
await ctx.sleep(order.deliveryDelay);
// 4. Trigger preparation
const preparationPromise = ctx.awakeable();
await ctx.run(() => restaurant.prepare(order.id, preparationPromise.id));
ctx.set("status", Status.IN_PREPARATION);
await preparationPromise.promise;
ctx.set("status", Status.SCHEDULING_DELIVERY);
// 5. Find a driver and start delivery
await ctx.objectClient(deliveryManager, order.id).startDelivery(order);
ctx.set("status", Status.DELIVERED);
}