diff --git a/patterns-use-cases/sagas/sagas-java/README.md b/patterns-use-cases/sagas/sagas-java/README.md
index ffba2b95..a3d4ad08 100644
--- a/patterns-use-cases/sagas/sagas-java/README.md
+++ b/patterns-use-cases/sagas/sagas-java/README.md
@@ -1,7 +1,7 @@
# SAGAs / Compensations
An example of a trip reservation workflow, using the SAGAs pattern to
-undo previous steps in csase of an error.
+undo previous steps in case of an error.
This is a minimal version of the holiday reservation demo in the
[Restate Holiday Repository](https://github.com/restatedev/restate-holiday).
@@ -12,4 +12,5 @@ Every step pushes a compensation action (an undo operation) to a stack.
in the case of an error, those operations are run.
The main requirement is that steps are implemented as journalled
-operations, like `ctx.run()` or `ctx.rpc()`.
+operations, like `ctx.run()` (e.g., direct calls to 3rd party APIs) or
+RPCs to other Restate-backed services (e.g., `FlightsClient.fromContex(ctx)`).
diff --git a/patterns-use-cases/sagas/sagas-java/src/main/java/dev/restate/patterns/BookingWorkflow.java b/patterns-use-cases/sagas/sagas-java/src/main/java/dev/restate/patterns/BookingWorkflow.java
new file mode 100644
index 00000000..481235cc
--- /dev/null
+++ b/patterns-use-cases/sagas/sagas-java/src/main/java/dev/restate/patterns/BookingWorkflow.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (c) 2024 - Restate Software, Inc., Restate GmbH
+ *
+ * This file is part of the Restate examples,
+ * which is released under the MIT license.
+ *
+ * You can find a copy of the license in the file LICENSE
+ * in the root directory of this repository or package or at
+ * https://github.com/restatedev/examples/
+ */
+
+package dev.restate.patterns;
+
+import dev.restate.patterns.activities.*;
+import dev.restate.sdk.WorkflowContext;
+import dev.restate.sdk.annotation.Workflow;
+import dev.restate.sdk.common.TerminalException;
+
+import java.util.ArrayList;
+import java.util.List;
+
+//
+// SAGAs / Compensations
+//
+// An example of a trip reservation workflow, using the SAGAs pattern to
+// undo previous steps in case of an error.
+//
+// Durable Execution's guarantee to run code to the end in the presence
+// of failures, and to deterministically recover previous steps from the
+// journal, makes SAGAs easy.
+// Every step pushes a compensation action (an undo operation) to a stack.
+// in the case of an error, those operations are run.
+//
+// The main requirement is that steps are implemented as journalled
+// operations, like `ctx.run()` or RPC calls/messages executed
+// through the Restate Context.
+//
+
+/**
+ * Trip reservation workflow which has been instrumented with compensations. The workflow tries to
+ * reserve the flight and the car rental before it processes the payment. If at any point one of
+ * the calls fails or gets cancelled, then the trip reservation workflow will undo all
+ * successfully completed steps by running the compensations.
+ *
+ *
Note: that the compensation logic is purely implemented in the user code and runs durably
+ * until it completes. Moreover, an invocation failure and an invocation cancellation are handled
+ * in the exact same way by the caller.
+ */
+@Workflow
+public class BookingWorkflow {
+
+ // The workflow parameters, like the car and flight to book, the
+ // payment details (card/token, amount, ...)
+ public record TravelBookingRequest( /* car, flights, payment info, ... */ ) { }
+
+ @Workflow
+ public void run(WorkflowContext context, TravelBookingRequest request) throws TerminalException {
+ // Create a list of compensations to run in case of a failure or cancellation.
+ final List compensations = new ArrayList<>();
+
+ try {
+ // Reserve the flights and let Restate remember the reservation ID
+ final var flightsRpcClient = FlightsClient.fromContext(context);
+ final String flightReservationId =
+ flightsRpcClient
+ .reserve(new Flights.FlightBookingRequest(request))
+ .await();
+ // Register the compensation to undo the flight reservation.
+ compensations.add(() -> flightsRpcClient.cancel(flightReservationId).await());
+
+ // Reserve the car and let Restate remember the reservation ID
+ final var carRentalRpcClient = CarRentalsClient.fromContext(context);
+ final String carReservationId =
+ carRentalRpcClient
+ .reserve(new CarRentals.CarRentalRequest(request))
+ .await();
+ // Register the compensation to undo the car rental reservation.
+ compensations.add(() -> carRentalRpcClient.cancel(carReservationId).await());
+
+ // call the payment service to make the payment and let Restate remember
+ // the payment ID
+ final var paymentRpcClient = PaymentClient.fromContext(context);
+ final String paymentId =
+ paymentRpcClient
+ .process(new Payment.PaymentRequest(request))
+ .await();
+ // Register the compensation to undo the payment.
+ compensations.add(() -> paymentRpcClient.refund(paymentId).await());
+
+ // confirm the reserved flight / rental
+ // failures here will still trigger the SAGA compensations
+ flightsRpcClient.confirm(flightReservationId).await();
+ carRentalRpcClient.confirm(carReservationId).await();
+
+ } catch (TerminalException e) {
+
+ // Run the compensations
+ for (Runnable compensation : compensations) {
+ compensation.run();
+ }
+
+ // rethrow error to fail this workflow
+ throw new TerminalException(
+ e.getCode(),
+ String.format(
+ "Failed to reserve the trip: %s. Ran %d compensations.",
+ e.getMessage(), compensations.size()));
+ }
+ }
+}
diff --git a/patterns-use-cases/sagas/sagas-java/src/main/java/dev/restate/patterns/Compensations.java b/patterns-use-cases/sagas/sagas-java/src/main/java/dev/restate/patterns/Compensations.java
deleted file mode 100644
index 49943795..00000000
--- a/patterns-use-cases/sagas/sagas-java/src/main/java/dev/restate/patterns/Compensations.java
+++ /dev/null
@@ -1,137 +0,0 @@
-/*
- * Copyright (c) 2024 - Restate Software, Inc., Restate GmbH
- *
- * This file is part of the Restate examples,
- * which is released under the MIT license.
- *
- * You can find a copy of the license in the file LICENSE
- * in the root directory of this repository or package or at
- * https://github.com/restatedev/examples/
- */
-
-package dev.restate.patterns;
-
-import dev.restate.sdk.Awaitable;
-import dev.restate.sdk.Context;
-import dev.restate.sdk.ObjectContext;
-import dev.restate.sdk.annotation.Handler;
-import dev.restate.sdk.annotation.Service;
-import dev.restate.sdk.annotation.VirtualObject;
-import dev.restate.sdk.common.TerminalException;
-import java.util.ArrayDeque;
-import java.util.Deque;
-import java.util.Iterator;
-
-//
-// SAGAs / Compensations
-//
-// An example of a trip reservation workflow, using the SAGAs pattern to
-// undo previous steps in case of an error.
-//
-// Durable Execution's guarantee to run code to the end in the presence
-// of failures, and to deterministically recover previous steps from the
-// journal, makes SAGAs easy.
-// Every step pushes a compensation action (an undo operation) to a stack.
-// in the case of an error, those operations are run.
-//
-// The main requirement is that steps are implemented as journalled
-// operations, like `ctx.run()` or RPC calls/messages executed
-// through the Restate Context.
-//
-
-public class Compensations {
-
- /**
- * Trip reservation workflow which has been instrumented with compensations. The workflow tries to
- * reserve the flight and the car rental before it processes the payment. If at any point one of
- * the calls fails or gets cancelled, then the trip reservation workflow will undo all
- * successfully completed steps by running the compensations.
- *
- * Note: that the compensation logic is purely implemented in the user code and runs durably
- * until it completes. Moreover, an invocation failure and an invocation cancellation are handled
- * in the exact same way by the caller.
- */
- @VirtualObject
- public static class Travels {
-
- public static class TravelBookingRequest { }
-
- @Handler
- public void reserve(ObjectContext context, TravelBookingRequest request) throws TerminalException {
- final FlightsClient.ContextClient flightsService = FlightsClient.fromContext(context);
- final CarRentalsClient.ContextClient carRentalService = CarRentalsClient.fromContext(context);
- final PaymentClient.ContextClient paymentService = PaymentClient.fromContext(context);
-
- // Create a list of compensations to run in case of a failure or cancellation.
- final Deque compensations = new ArrayDeque<>();
-
- try {
- final String flightBookingId =
- flightsService
- .reserve(new Flights.FlightBookingRequest())
- .await();
- // Register the compensation to undo the flight reservation.
- compensations.add(() -> flightsService.cancel(flightBookingId).await());
-
- final String carRentalId =
- carRentalService
- .reserve(new CarRentals.CarRentalBookingRequest())
- .await();
- // Register the compensation to undo the car rental reservation.
- compensations.add(() -> carRentalService.cancel(carRentalId).await());
-
- final String paymentId =
- paymentService
- .process(new Payment.PaymentRequest())
- .await();
- // Register the compensation to undo the payment.
- compensations.add(() -> paymentService.refund(paymentId).await());
-
- Awaitable.all(
- flightsService.confirm(flightBookingId), carRentalService.confirm(carRentalId))
- .await();
- } catch (TerminalException e) {
- // Run the compensations in reverse order
- final Iterator compensationsIterator = compensations.descendingIterator();
-
- while (compensationsIterator.hasNext()) {
- compensationsIterator.next().run();
- }
-
- throw new TerminalException(
- e.getCode(),
- String.format(
- "Failed to reserve the trip: %s. Ran %d compensations.",
- e.getMessage(), compensations.size()));
- }
- }
- }
-
- // --- Interfaces for Flights, CarRental and Payment components
-
- @Service(name = "Flights")
- interface Flights {
- class FlightBookingRequest { }
-
- @Handler String reserve(Context context, FlightBookingRequest request);
- @Handler void confirm(Context context, String flightBookingId);
- @Handler void cancel(Context context, String flightBookingId);
- }
-
- @Service(name = "CarRentals")
- interface CarRentals {
- class CarRentalBookingRequest { }
-
- @Handler String reserve(Context context, CarRentalBookingRequest request);
- @Handler void confirm(Context context, String carRentalBookingId);
- @Handler void cancel(Context context, String carRentalBookingId);
- }
-
- @Service(name = "Payment")
- interface Payment {
- class PaymentRequest { }
-
- @Handler String process(Context context, PaymentRequest request);
- @Handler void refund(Context context, String paymentId);
- }
-}
diff --git a/patterns-use-cases/sagas/sagas-java/src/main/java/dev/restate/patterns/activities/CarRentals.java b/patterns-use-cases/sagas/sagas-java/src/main/java/dev/restate/patterns/activities/CarRentals.java
new file mode 100644
index 00000000..6b7e81d3
--- /dev/null
+++ b/patterns-use-cases/sagas/sagas-java/src/main/java/dev/restate/patterns/activities/CarRentals.java
@@ -0,0 +1,34 @@
+package dev.restate.patterns.activities;
+
+import dev.restate.patterns.BookingWorkflow.TravelBookingRequest;
+import dev.restate.sdk.Context;
+import dev.restate.sdk.annotation.Handler;
+import dev.restate.sdk.annotation.Service;
+
+import java.util.UUID;
+
+@Service
+public class CarRentals {
+
+ public record CarRentalRequest(TravelBookingRequest req /* rental details */) {}
+
+ @Handler
+ public String reserve(Context ctx, CarRentalRequest request) {
+ // this should implement the communication with the rental
+ // provider's APIs
+ // just return a mock random id representing the reservation
+ return "car-" + UUID.randomUUID().toString();
+ }
+
+ @Handler
+ public void confirm(Context ctx, String carRentalBookingId) {
+ // this should implement the communication with the rental
+ // provider's APIs
+ }
+
+ @Handler
+ public void cancel(Context ctx, String carRentalBookingId) {
+ // this should implement the communication with the rental
+ // provider's APIs
+ }
+}
diff --git a/patterns-use-cases/sagas/sagas-java/src/main/java/dev/restate/patterns/activities/Flights.java b/patterns-use-cases/sagas/sagas-java/src/main/java/dev/restate/patterns/activities/Flights.java
new file mode 100644
index 00000000..0115b6d1
--- /dev/null
+++ b/patterns-use-cases/sagas/sagas-java/src/main/java/dev/restate/patterns/activities/Flights.java
@@ -0,0 +1,34 @@
+package dev.restate.patterns.activities;
+
+import dev.restate.patterns.BookingWorkflow.TravelBookingRequest;
+import dev.restate.sdk.Context;
+import dev.restate.sdk.annotation.Handler;
+import dev.restate.sdk.annotation.Service;
+
+import java.util.UUID;
+
+@Service
+public class Flights {
+
+ public record FlightBookingRequest(TravelBookingRequest req /* flight number, class, ... */) {}
+
+ @Handler
+ public String reserve(Context ctx, FlightBookingRequest request) {
+ // this should implement the communication with the rental
+ // provider's APIs
+ // just return a mock random id representing the reservation
+ return "car-" + UUID.randomUUID().toString();
+ }
+
+ @Handler
+ public void confirm(Context ctx, String flightBookingId) {
+ // this should implement the communication with the rental
+ // provider's APIs
+ }
+
+ @Handler
+ public void cancel(Context ctx, String flightBookingId) {
+ // this should implement the communication with the rental
+ // provider's APIs
+ }
+}
diff --git a/patterns-use-cases/sagas/sagas-java/src/main/java/dev/restate/patterns/activities/Payment.java b/patterns-use-cases/sagas/sagas-java/src/main/java/dev/restate/patterns/activities/Payment.java
new file mode 100644
index 00000000..f8be7e89
--- /dev/null
+++ b/patterns-use-cases/sagas/sagas-java/src/main/java/dev/restate/patterns/activities/Payment.java
@@ -0,0 +1,29 @@
+package dev.restate.patterns.activities;
+
+import dev.restate.patterns.BookingWorkflow.TravelBookingRequest;
+import dev.restate.sdk.Context;
+import dev.restate.sdk.annotation.Handler;
+import dev.restate.sdk.annotation.Service;
+
+import java.util.UUID;
+
+@Service
+public class Payment {
+
+ public record PaymentRequest(TravelBookingRequest req /* paymentToken, amount, ... */ ) {}
+
+ @Handler
+ String process(Context context, PaymentRequest request) {
+ // this should implement the actual payment processing, or communication
+ // to the external provider's APIs
+ // just return a mock random id representing the payment
+ return UUID.randomUUID().toString();
+ }
+
+ @Handler
+ public void refund(Context context, String paymentId) {
+ // refund the payment identified by this paymentId
+ // this should implement the actual payment processing, or communication
+ // to the external provider's APIs
+ }
+}
\ No newline at end of file