Skip to main content

Coming from Restate

Are you coming from experience with Restate?

This guide will help you understand the differences and similarities between Resonate and Restate, and how to translate Restate concepts to Resonate's programming model.

Overview

Both Resonate and Restate provide durable execution for reliable distributed applications. However, they differ significantly in their approach:

AspectRestateResonate
Core modelThree service types (Basic Service, Virtual Object, Workflow)Just functions - no proprietary types
Server requirementAlways requires Restate ServerOptional - zero-dependency local mode available
Programming modelctx.run() wrappers, journaling/replayDistributed async/await (native async syntax)
DeploymentServices → Endpoints → Registration with Restate ServerDeploy anywhere, connect when ready
State managementBuilt-in K/V store in Virtual ObjectsDurable Promises (explicit, flexible)
Learning curveMust understand 3 service types + deployment modelFamiliar async/await patterns

Philosophy

Restate's approach: Define your application using three different service types depending on your needs (stateless handlers, stateful objects, or workflows), wrap non-deterministic operations in ctx.run(), and deploy behind endpoints that register with the Restate Server.

Resonate's approach: Write normal async/await code. Resonate's distributed async/await automatically makes it durable without wrapper functions or service type abstractions.

Programming model

Service types become just functions

Restate has three service types you must choose between:

  • Basic Service: Stateless handlers
  • Virtual Object: Stateful entity with a key
  • Workflow: Once-per-ID execution with signals/queries

In Resonate, there's no such distinction. You write functions. That's it.

Restate Basic Service
TypeScript
import * as restate from "@restatedev/restate-sdk";

export const myService = restate.service({
name: "MyService",
handlers: {
processOrder: async (ctx: restate.Context, order: Order) => {
const payment = await ctx.run(() => processPayment(order));
const shipment = await ctx.run(() => scheduleShipment(order));
return { payment, shipment };
},
},
});

restate.serve({ services: [myService] });

Key difference: No service wrapper, no handler object, no explicit serving. Just register the function.

Durable steps

Both systems wrap non-deterministic operations to make them durable.

Restate durable step
TypeScript
const result = await ctx.run<string>(async () => doDbRequest());

Both achieve the same goal - making non-deterministic operations durable - but Resonate's generator-based approach enables automatic checkpointing of the entire call stack.

State management

Restate: Built-in K/V state in Virtual Objects and Workflows

Restate Virtual Object with state
TypeScript
export const userAccount = restate.object({
name: "UserAccount",
handlers: {
deposit: async (ctx: restate.ObjectContext, amount: number) => {
const balance = (await ctx.get<number>("balance")) ?? 0;
await ctx.set("balance", balance + amount);
return balance + amount;
},
},
});

Resonate: Use Durable Promises for state or connect to external stores

Resonate with external state
TypeScript
function* deposit(ctx: Context, userId: string, amount: number) {
// Option 1: External database (your choice of DB)
const balance = yield* ctx.run(getBalance, userId);
const newBalance = balance + amount;
yield* ctx.run(saveBalance, userId, newBalance);

// Option 2: Durable Promise for workflow state
const balancePromise = yield* ctx.promise(`balance-${userId}`);
// ... resolve when ready

return newBalance;
}

Resonate doesn't prescribe state storage - use what fits your architecture. Durable Promises provide coordination primitives when you need them.

Workflows

Restate Workflows are a special service type for once-per-ID execution:

Restate Workflow
TypeScript
export const orderWorkflow = restate.workflow({
name: "OrderWorkflow",
handlers: {
run: async (ctx: restate.WorkflowContext, order: Order) => {
// Main workflow logic
const approved = await ctx.promise<boolean>("approval");
if (approved) {
await ctx.run(() => fulfillOrder(order));
}
return "complete";
},

approve: async (ctx: restate.WorkflowSharedContext) => {
await ctx.promise("approval").resolve(true);
},
},
});

In Resonate, this is just a function with a Durable Promise:

Resonate workflow function
TypeScript
function* orderWorkflow(ctx: Context, orderId: string, order: Order) {
const approvalPromise = yield* ctx.promise(`approval-${orderId}`);
const approved = yield* approvalPromise;

if (approved) {
yield* ctx.run(fulfillOrder, order);
}

return "complete";
}

// Approve from anywhere
async function approveOrder(orderId: string) {
await resonate.promises.resolve({
id: `approval-${orderId}`,
value: true
});
}

No special workflow service type, no separate handler methods - just functions and promises.

Durable Promise IDs

Each Durable Promise has a unique ID (like approval-${orderId}) that acts as the idempotency key. The same promise ID will always resolve to the same value, making workflows resumable and ensuring exactly-once semantics.

Deployment and infrastructure

Restate's model

  1. Deploy services to your infrastructure (containers, Lambda, etc.)
  2. Start Restate Server (single binary or cluster)
  3. Register each service endpoint with the server:
    Register deployment with Restate
    Shell
    restate deployments register http://my-service:9080
  4. Route requests through Restate Server

Always requires Restate Server as a proxy/coordinator in front of services.

Resonate's model

Development: Zero-dependency mode - no server required

Local development mode
TypeScript
const resonate = new Resonate(); // Runs locally, in-memory

Production: Connect to Resonate Server when ready

Production mode
TypeScript
const resonate = new Resonate({
url: "http://resonate-server:8001"
});
Zero-dependency development

You can develop and test without any infrastructure. Add the server when you need distributed coordination, not before.

Human-in-the-loop

Both systems support human-in-the-loop patterns.

Restate: Uses workflow promises or signals

Restate workflow promise
TypeScript
// In workflow
const decision = await ctx.promise<string>("user-decision");

// From external handler
await ctx.promise("user-decision").resolve("approved");

Resonate: Uses Durable Promises

Resonate Durable Promise
TypeScript
// In function
const approvalPromise = yield* ctx.promise("approval-123");
const decision = yield* approvalPromise;

// From anywhere
await resonate.promises.resolve({ id: "approval-123", value: "approved" });

Same concept, similar API. Resonate's version works with any function, not just Workflows.

Migration path

If you're moving from Restate to Resonate:

  1. Start simple: Remove service type abstractions - your handlers become functions
  2. Adjust syntax: Change await ctx.run(() => fn()) to yield* ctx.run(fn)
  3. Rethink state: If using Virtual Object state, decide whether to use external DB or Durable Promises
  4. Simplify deployment: Remove endpoint registration step - just deploy and connect
  5. Test locally: Take advantage of zero-dependency mode for faster iteration

Why choose Resonate

Resonate offers:

  • Simpler primitives - Just functions, not three service types
  • Zero-dependency local development - Start building without infrastructure
  • Familiar async/await patterns - Distributed async/await feels natural
  • Flexible state management - Use your choice of database
  • No vendor lock-in - Avoid proprietary abstractions
  • Gentler learning curve - Less to learn, faster to ship

Example: Order processing saga

Side-by-side comparison of the same workflow:

Restate order saga
TypeScript
export const orderSaga = restate.service({
name: "OrderSaga",
handlers: {
processOrder: async (ctx: restate.Context, order: Order) => {
const payment = await ctx.run(() => chargePayment(order));

try {
const inventory = await ctx.run(() => reserveInventory(order));
const shipment = await ctx.run(() => scheduleShipment(order, inventory));
return { success: true, shipment };
} catch (error) {
await ctx.run(() => refundPayment(payment));
throw error;
}
},
},
});

Same logic, fewer abstractions. The Resonate version is a plain function with generator syntax.

Concepts mapping

Restate ConceptResonate Equivalent
Basic ServiceRegistered function
Virtual ObjectFunction + external state or Durable Promise
WorkflowFunction with Durable Promises
HandlerFunction
ctx.run()ctx.run() (with generator syntax)
ctx.promise() (workflow)ctx.promise() (any function)
ctx.sleep()ctx.sleep()
ctx.get/set (state)External DB or Durable Promise
Service endpointFunction registration
Deployment registrationOptional server connection

Next steps


Questions or feedback? This comparison is a work in progress. If you're coming from Restate and have questions about concepts we haven't covered, let us know in Discord!