Coming from Restate

Compare Resonate with Restate to understand how the platforms differ.

SDK version

This page reflects @resonatehq/sdk v0.10.1 (current on npm) and resonate-sdk v0.6.7 for Python.

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
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
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
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
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();
  // ... 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
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
function* orderWorkflow(ctx: Context, orderId: string, order: Order) {
  // ctx.promise() creates a child promise with ID `${ctx.id}.<seq>`.
  // Because the workflow was invoked with orderId as the execution ID,
  // the approval promise ID is deterministic: `${orderId}.0`.
  const approvalPromise = yield* ctx.promise<boolean>();
  const approved = yield* approvalPromise;

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

  return "complete";
}

// Approve from anywhere — the caller reconstructs the ID from orderId.
async function approveOrder(orderId: string) {
  await resonate.promises.resolve(`${orderId}.0`, { data: JSON.stringify(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
    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
const resonate = new Resonate(); // Runs locally, in-memory

Production: Connect to Resonate Server when ready

Production mode
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
// 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
// In function — ctx.promise() creates `${ctx.id}.<seq>` deterministically.
const approvalPromise = yield* ctx.promise<string>();
const decision = yield* approvalPromise;

// From anywhere — use the caller's known execution ID (e.g. `order-123`)
// to reconstruct the approval promise ID as `${orderId}.<seq>`.
await resonate.promises.resolve(`${orderId}.0`, { data: JSON.stringify("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
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!