A Saga is a design pattern for handling transactions that span multiple services. It breaks the process into a sequence of local operations, each with a corresponding compensating action. If a failure occurs partway through, these compensations are triggered to undo completed steps, ensuring your system stays consistent even when things go wrong.

How does Restate help?

Restate makes implementing resilient sagas simple:
  • Durable Execution: Restate guarantees completion and automatically retries from failure points. No manual state tracking or retry logic needed
  • Code-first approach: Define sagas using regular code, no DSLs required
Sagas UI

Example

A travel booking workflow: book a flight, rent a car, then book a hotel. If any step fails (e.g. hotel full), we roll back previous steps to maintain consistency. Sagas example diagram Implementation:
  • Wrap business logic in a try-block, throw terminal errors for compensation cases
  • Add compensations to a list for each step
  • In catch block, run compensations in reverse order and rethrow
Note: Golang uses defer for compensations.
const bookingWorkflow = restate.service({
  name: "BookingWorkflow",
  handlers: {
    run: async (ctx: restate.Context, req: BookingRequest) => {
      const { customerId, flight, car, hotel } = req;
      const compensations = [];

      try {
        compensations.push(() => ctx.run("Cancel flight", () => flightClient.cancel(customerId)));
        await ctx.run("Book flight", () => flightClient.book(customerId, flight));

        compensations.push(() => ctx.run("Cancel car", () => carRentalClient.cancel(customerId)));
        await ctx.run("Book car", () => carRentalClient.book(customerId, car));

        compensations.push(() => ctx.run("Cancel hotel", () => hotelClient.cancel(customerId)));
        await ctx.run("Book hotel", () => hotelClient.book(customerId, hotel));
      } catch (e) {
        if (e instanceof restate.TerminalError) {
          for (const compensation of compensations.reverse()) {
            await compensation();
          }
        }
        throw e;
      }
    },
  },
});

restate.serve({
  services: [bookingWorkflow],
  port: 9080,
});
View on GitHub: TS / Java / Kotlin / Python / Go
This pattern is implementable with any of our SDKs. We are still working on translating all patterns to all SDK languages. If you need help with a specific language, please reach out to us via Discord or Slack.

When to use Sagas

Restate automatically retries transient failures (network hiccups, temporary outages). For non-transient failures, sagas are essential:
  1. Business logic failures: When failures are business decisions (e.g. “Hotel is full”), retrying won’t help. Throw a terminal error to trigger compensations.
  2. User/system cancellations: When you cancel long-running invocations, sagas undo previous operations to maintain consistency.

Running the example

1

Download the example

restate example typescript-patterns-use-cases && cd typescript-patterns-use-cases
2

Start the Restate Server

restate-server
3

Start the Service

npx tsx watch ./src/sagas/booking_workflow.ts
4

Register the services

restate deployments register localhost:9080
5

Send a request

curl localhost:8080/BookingWorkflow/run --json '{
        "flight": {
            "flightId": "12345",
            "passengerName": "John Doe"
        },
            "car": {
            "pickupLocation": "Airport",
            "rentalDate": "2024-12-16"
        },
            "hotel": {
            "arrivalDate": "2024-12-16",
            "departureDate": "2024-12-20"
        }
    }'
6

Check the UI or service logs

See in the Restate UI (localhost:9070) how all steps were executed, and how the compensations were triggered because the hotel was full.Sagas UI

Advanced: Idempotency and compensations

Sagas in Restate are flexible and powerful since they’re implemented in user code. However, you need to make sure compensations are idempotent. The example uses customer ID for idempotency, preventing duplicate bookings on retries. The API provider deduplicates requests based on this ID. Different APIs require different approaches:
  1. Two-phase APIs: First reserve, then confirm or cancel. Register the compensation after reservation, when you have the resource ID. This type of API usually auto-cancels reservations after a timeout.
const bookingId = await ctx.run(() =>
  flightClient.reserve(customerId, flight)
);
compensations.push(() => ctx.run(() => flightClient.cancel(bookingId)));

// ... do other work, like reserving a car, etc. ...

await ctx.run(() => flightClient.confirm(bookingId));
  1. One-shot APIs with idempotency key: Generate idempotency key, persist in Restate, register compensation (e.g. refund), then do action (e.g. charge). Register compensation first in case action succeeded but confirmation was lost.
const paymentId = ctx.rand.uuidv4();
compensations.push(() => ctx.run(() => paymentClient.refund(paymentId)));
await ctx.run(() => paymentClient.charge(paymentInfo, paymentId));