Skip to content

Commit 8b81656

Browse files
authored
Update saga examples (#288)
* remove .idea * Java sagas: use ctx.run * Java sagas cleanup text * Update readme * spotless formatting * Simplify sagas * Simplify sagas * Simplify sagas * Simplify kotlin sagas * Simplify kotlin sagas * Format kotlin code snippets * Simplify TS saga example * Format TS patterns * Add other guides * Simplify python sagas * Simplify go saga * Cron job example (#289) * Cron job example * Cron job example * Add cron example * Improve readme * Add to main readme * Fix determinism of time calculation * Improve readme * Add Java cron example * Add readme and comments to Java cron example * Update readmes * Restructure Java cron example to single class and rename to CronJobInitiator and CronJob * Update cron example comments * Clean up Java code * Clean up TS + Java cron example code * Move main class and adapt readme * Fix serde TS cron * Add guide note * Feedback * Adapt flights to flight * Adapt flights to flight * Feedback and formatting
1 parent 513922d commit 8b81656

File tree

175 files changed

+2597
-2450
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

175 files changed

+2597
-2450
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ Or have a look at the general catalog below:
5858
|---------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
5959
| <a id="durable-rpc">Durable RPC, Idempotency & Concurrency</a> | [<img src="https://skillicons.dev/icons?i=ts" width="24" height="24">](typescript/patterns-use-cases/README.md#durable-rpc-idempotency--concurrency) [<img src="https://skillicons.dev/icons?i=go" width="24" height="24">](go/patterns-use-cases/README.md#durable-rpc-idempotency--concurrency) [<img src="https://skillicons.dev/icons?i=python&theme=light" width="24" height="24">](python/patterns-use-cases/README.md#durable-rpc-idempotency--concurrency) [<img src="https://skillicons.dev/icons?i=java&theme=light" width="24" height="24">](java/patterns-use-cases/README.md#durable-rpc-idempotency--concurrency) |
6060
| <a id="message-queue">\(Delayed\) Message Queue</a> | [<img src="https://skillicons.dev/icons?i=ts" width="24" height="24">](typescript/patterns-use-cases/README.md#delayed-message-queue) [<img src="https://skillicons.dev/icons?i=go" width="24" height="24">](go/patterns-use-cases/README.md#delayed-message-queue) [<img src="https://skillicons.dev/icons?i=python&theme=light" width="24" height="24">](python/patterns-use-cases/README.md#delayed-message-queue) [<img src="https://skillicons.dev/icons?i=java&theme=light" width="24" height="24">](java/patterns-use-cases/README.md#delayed-message-queue) [<img src="https://skillicons.dev/icons?i=kotlin&theme=light" width="24" height="24">](kotlin/patterns-use-cases/README.md#delayed-message-queue) |
61+
| <a id="cron">Cron Jobs</a> | [<img src="https://skillicons.dev/icons?i=ts" width="24" height="24">](typescript/patterns-use-cases/README.md#cron-jobs) [<img src="https://skillicons.dev/icons?i=java&theme=light" width="24" height="24">](java/patterns-use-cases/README.md#cron-jobs) |
6162
| <a id="webhook-callbacks">Webhook Callbacks</a> | [<img src="https://skillicons.dev/icons?i=ts" width="24" height="24">](typescript/patterns-use-cases/README.md#webhook-callbacks) [<img src="https://skillicons.dev/icons?i=go" width="24" height="24">](go/patterns-use-cases/README.md#webhook-callbacks) |
6263
| <a id="database-interaction">Database Interaction Patterns</a> | [<img src="https://skillicons.dev/icons?i=ts" width="24" height="24">](typescript/patterns-use-cases/README.md#database-interaction-patterns) |
6364
| <a id="sync-to-async">Convert Sync Tasks to Async</a> | [<img src="https://skillicons.dev/icons?i=ts" width="24" height="24">](typescript/patterns-use-cases/README.md#convert-sync-tasks-to-async) [<img src="https://skillicons.dev/icons?i=go" width="24" height="24">](go/patterns-use-cases/README.md#convert-sync-tasks-to-async) [<img src="https://skillicons.dev/icons?i=python&theme=light" width="24" height="24">](python/patterns-use-cases/README.md#convert-sync-tasks-to-async) [<img src="https://skillicons.dev/icons?i=java&theme=light" width="24" height="24">](java/patterns-use-cases/README.md#convert-sync-tasks-to-async) |
@@ -73,7 +74,7 @@ Or have a look at the general catalog below:
7374
| <a id="promise-as-a-service">Durable Promises as a Service</a> | [<img src="https://skillicons.dev/icons?i=ts" width="24" height="24">](typescript/patterns-use-cases/README.md#durable-promises-as-a-service) |
7475
| <a id="priority-queue">Priority Queue</a> | [<img src="https://skillicons.dev/icons?i=ts" width="24" height="24">](typescript/patterns-use-cases/README.md#priority-queue) |
7576
| <a id="rate-limiting">Rate Limiting</a> | [<img src="https://skillicons.dev/icons?i=ts" width="24" height="24">](typescript/patterns-use-cases/README.md#rate-limiting) [<img src="https://skillicons.dev/icons?i=go" width="24" height="24">](go/patterns-use-cases/README.md#rate-limiting) |
76-
| <a id="ai">AI: agents, LLM calls, MCP, A2A,...</a> | [AI examples repo](https://github.com/restatedev/ai-examples) |
77+
| <a id="ai">AI: agents, LLM calls, MCP, A2A,...</a> | [AI examples repo](https://github.com/restatedev/ai-examples) |
7778

7879
#### Integrations
7980

go/patterns-use-cases/README.md

Lines changed: 41 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -115,69 +115,75 @@ Have a look at the logs to see how the execution switches from synchronously wai
115115

116116
## Sagas
117117
[<img src="https://raw.githubusercontent.com/restatedev/img/refs/heads/main/show-code.svg">](src/sagas/bookingworkflow.go)
118+
[<img src="https://raw.githubusercontent.com/restatedev/img/refs/heads/main/read-guide.svg">](https://docs.restate.dev/guides/sagas)
118119

119-
An example of a trip reservation workflow, using the saga pattern to undo previous steps in case of an error.
120+
When building distributed systems, it is crucial to ensure that the system remains consistent even in the presence of failures.
121+
One way to achieve this is by using the Saga pattern.
120122

121-
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.
122-
Every step pushes a compensation action (an undo operation) to a stack. In the case of an error, those operations are run.
123+
Sagas are a way to manage transactions that span multiple services.
124+
They allow you to run compensations when your code crashes halfway through.
125+
This way, you can ensure that your system remains consistent even in the presence of failures.
123126

124-
The main requirement is that steps are implemented as journaled operations, like `restate.Run()` or RPC/messaging.
127+
Restate guarantees that sagas run to completion. It will handle retries and failures, and ensure that compensations are executed successfully.
125128

126-
The example shows two ways you can implement the compensation, depending on the characteristics of the API/system you interact with.
127-
1. **Two-phase commit**: The reservation is created and then confirmed or cancelled. The compensation executes 'cancel' and is added after the reservation is created.
128-
2. **Idempotency key**: The payment is made in one shot and supplies an ID. The compensation is added before the payment is made and uses the same ID.
129+
<img src="img/saga_diagram.svg" width="500" alt="Saga Workflow">
129130

130131
Note that the compensating actions need to be idempotent.
131132

133+
<img src="img/saga_journal.png" width="1200px" alt="Saga Journal">
134+
132135
<details>
133136
<summary><strong>Running the example</strong></summary>
134137

135-
1. [Start the Restate Server](https://docs.restate.dev/develop/local_dev) in a separate shell: `restate-server`
136-
2. Start the service: `go run ./src/sagas`
137-
3. Register the services (with `--force` to override the endpoint during **development**): `restate -y deployments register --force localhost:9080`
138+
1. [Start the Restate Server](https://docs.restate.dev/develop/local_dev) in a separate shell:
139+
```shell
140+
restate-server
141+
```
142+
2. Start the service:
143+
```shell
144+
go run ./src/sagas
145+
```
146+
3. Register the services (with `--force` to override the endpoint during **development**):
147+
```shell
148+
restate -y deployments register --force localhost:9080
149+
```
138150

139151
Have a look at the logs to see how the compensations run in case of a terminal error.
140152

141153
Start the workflow:
142154
```shell
143-
curl -X POST localhost:8080/BookingWorkflow/trip123/Run -H 'content-type: application/json' -d '{
144-
"flights": {
145-
"flight_id": "12345",
146-
"passenger_name": "John Doe"
155+
curl localhost:8080/BookingWorkflow/Run --json '{
156+
"customerId": "12345",
157+
"flight": {
158+
"flightId": "12345",
159+
"passengerName": "John Doe"
147160
},
148161
"car": {
149-
"pickup_location": "Airport",
150-
"rental_date": "2024-12-16"
162+
"pickupLocation": "Airport",
163+
"rentalDate": "2024-12-16"
151164
},
152-
"payment_info": {
153-
"card_number": "4111111111111111",
154-
"amount": 1500
165+
"hotel": {
166+
"arrivalDate": "2024-12-16",
167+
"departureDate": "2024-12-20"
155168
}
156169
}'
157170
```
158171

172+
159173
Have a look at the logs to see the cancellations of the flight and car booking in case of a terminal error.
160174

161175
<details>
162176
<summary>View logs</summary>
163177

164178
```
165-
2025/01/06 16:16:02 INFO Handling invocation method=BookingWorkflow/Run invocationID=inv_17l9ZLwBY3bz6HEIybYB6Rh9SbV6khuc0N
166-
2025/01/06 16:16:02 INFO Handling invocation method=Flights/Reserve invocationID=inv_1kNkgfEJjWp67I8WxNRHZN79XZprWqPWp3
167-
2025/01/06 16:16:02 INFO Flight reserved: 8685229b-c219-466f-9a70-9f54b968a1b9
168-
2025/01/06 16:16:02 INFO Invocation completed successfully method=Flights/Reserve invocationID=inv_1kNkgfEJjWp67I8WxNRHZN79XZprWqPWp3
169-
2025/01/06 16:16:02 INFO Handling invocation method=CarRentals/Reserve invocationID=inv_1cXn5IBHJhEK7ihQnoXIX8rVLvWWAi27EB
170-
2025/01/06 16:16:02 INFO Car reserved:2b5be5c4-944c-48a4-abb3-e0e0039151e9
171-
2025/01/06 16:16:02 INFO Invocation completed successfully method=CarRentals/Reserve invocationID=inv_1cXn5IBHJhEK7ihQnoXIX8rVLvWWAi27EB
172-
2025/01/06 16:16:02 ERROR This payment should never be accepted! Aborting booking.
173-
2025/01/06 16:16:02 INFO Handling invocation method=Flights/Cancel invocationID=inv_1d4KgHFg2EFF62ccgILiNAgPwKx4tmskyl
174-
2025/01/06 16:16:02 INFO Flight cancelled: 8685229b-c219-466f-9a70-9f54b968a1b9
175-
2025/01/06 16:16:02 INFO Invocation completed successfully method=Flights/Cancel invocationID=inv_1d4KgHFg2EFF62ccgILiNAgPwKx4tmskyl
176-
2025/01/06 16:16:02 INFO Handling invocation method=CarRentals/Cancel invocationID=inv_15QXMdt8GLYU18PoNhVICXbqRg0x9lQsIp
177-
2025/01/06 16:16:02 INFO Car cancelled2b5be5c4-944c-48a4-abb3-e0e0039151e9
178-
2025/01/06 16:16:02 INFO Invocation completed successfully method=CarRentals/Cancel invocationID=inv_15QXMdt8GLYU18PoNhVICXbqRg0x9lQsIp
179-
2025/01/06 16:16:02 INFO Refunded payment: e4eac4a9-47c9-4087-9502-cb0fff1218c6
180-
2025/01/06 16:16:02 INFO Invocation completed successfully method=BookingWorkflow/Run invocationID=inv_17l9ZLwBY3bz6HEIybYB6Rh9SbV6khuc0N
179+
2025/05/29 21:23:19 INFO Handling invocation method=BookingWorkflow/Run invocationID=inv_1jDSzGn5OKSm41ndgD8QcKpfPtPUSEgEM1
180+
2025/05/29 21:23:19 INFO Flight reserved for customer: 12345
181+
2025/05/29 21:23:19 INFO Car reserved for customer: 12345
182+
2025/05/29 21:23:19 ERROR [👻 SIMULATED] This hotel is fully booked!
183+
2025/05/29 21:23:19 INFO Flight cancelled for customer:12345
184+
2025/05/29 21:23:19 INFO Car cancelled for customer:12345
185+
2025/05/29 21:23:19 INFO Hotel cancelled for customer:12345
186+
2025/05/29 21:23:19 ERROR Invocation returned a terminal failure method=BookingWorkflow/Run invocationID=inv_1jDSzGn5OKSm41ndgD8QcKpfPtPUSEgEM1 err="[500] [👻 SIMULATED] This hotel is fully booked!"
181187
```
182188

183189
</details>

go/patterns-use-cases/img/saga_diagram.svg

Lines changed: 2 additions & 0 deletions
Loading
86.7 KB
Loading

go/patterns-use-cases/src/durablerpc/client/client.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,25 +19,25 @@ func ReserveProduct(productId string, reservationId string) {
1919
// Durable RPC call to the product service
2020
// Restate registers the request and makes sure it runs to completion exactly once
2121
// This is a call to Virtual Object so we can be sure only one reservation is made concurrently
22-
url := fmt.Sprintf("%s/ProductService/%s/Reserve", RESTATE_URL, productId)
22+
url := fmt.Sprintf("%s/ProductService/%s/Book", RESTATE_URL, productId)
2323
req, err := http.NewRequest("POST", url, nil)
2424
if err != nil {
25-
slog.Error("Reserve product failed", "err", err.Error())
25+
slog.Error("Book product failed", "err", err.Error())
2626
return
2727
}
2828
// use a stable uuid as an idempotency key; Restate deduplicates for us
2929
req.Header.Set("idempotency-key", reservationId)
3030

3131
resp, err := client.Do(req)
3232
if err != nil {
33-
slog.Error("Reserve product failed", "err", err.Error())
33+
slog.Error("Book product failed", "err", err.Error())
3434
return
3535
}
3636
defer resp.Body.Close()
3737

3838
body, err := io.ReadAll(resp.Body)
3939
if err != nil {
40-
slog.Error("Reserve product failed", "err", err.Error())
40+
slog.Error("Book product failed", "err", err.Error())
4141
return
4242
}
4343

go/patterns-use-cases/src/sagas/activities/carrentalservice.go

Lines changed: 0 additions & 29 deletions
This file was deleted.

go/patterns-use-cases/src/sagas/activities/flightservice.go

Lines changed: 0 additions & 29 deletions
This file was deleted.

go/patterns-use-cases/src/sagas/activities/paymentclient.go

Lines changed: 0 additions & 34 deletions
This file was deleted.

0 commit comments

Comments
 (0)