Skip to content

Commit 27a01ea

Browse files
authored
Create 2023-05-21-saga-pattern-and-laravel-workflow.md
1 parent ac468ec commit 27a01ea

File tree

1 file changed

+160
-0
lines changed

1 file changed

+160
-0
lines changed
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
---
2+
slug: saga-pattern-and-laravel-workflow
3+
title: "Saga Pattern and Laravel Workflow"
4+
authors:
5+
name: Richard
6+
title: Core Team
7+
url: https://github.com/rmcdaniel
8+
image_url: https://github.com/rmcdaniel.png
9+
tags: [sagas, microservices]
10+
---
11+
12+
Suppose we are working on a Laravel application that offers trip booking. A typical trip booking involves several steps such as:
13+
14+
1. Booking a flight.
15+
2. Booking a hotel.
16+
3. Booking a rental car.
17+
18+
Our customers expect an all-or-nothing transaction — it doesn’t make sense to book a hotel without a flight. Now imagine each of these booking steps being represented by a distinct API.
19+
20+
Together, these steps form a distributed transaction spanning multiple services and databases. For a successful booking, all three APIs must accomplish their individual local transactions. If any step fails, the preceding successful transactions need to be reversed in an orderly fashion. With money and bookings at stake, we can’t merely erase prior transactions — we need an immutable record of attempts and failures. Thus, we should compile a list of compensatory actions for execution in the event of a failure.
21+
22+
Prerequisites
23+
=============
24+
25+
To follow this tutorial, you should:
26+
27+
1. Set up a local development environment for Laravel Workflow applications in PHP or use the sample app in a GitHub [codespace](https://github.com/laravel-workflow/sample-app).
28+
2. Familiarize yourself with the basics of starting a Laravel Workflow project by reviewing the [documentation](https://laravel-workflow.com/docs/installation).
29+
3. Review the [Saga architecture pattern](https://microservices.io/patterns/data/saga.html).
30+
31+
Sagas are an established design pattern for managing complex, long-running operations:
32+
33+
1. A Saga manages transactions using a sequence of local transactions.
34+
2. A local transaction is a work unit performed by a saga participant (a microservice).
35+
3. Each operation in the Saga can be reversed by a compensatory transaction.
36+
4. The Saga pattern assures that all operations are either completed successfully or the corresponding compensation transactions are run to reverse any completed work.
37+
38+
Laravel Workflow provides inherent support for the Saga pattern, simplifying the process of handling rollbacks and executing compensatory transactions.
39+
40+
Booking Saga Flow
41+
=================
42+
43+
We will visualize the Saga pattern for our trip booking scenario with a diagram.
44+
45+
![trip booking saga](https://miro.medium.com/v2/1*WD1_N0mIdeDtIPycKQj6yQ.png)
46+
47+
Workflow Implementation
48+
-----------------------
49+
50+
We’ll begin by creating a high-level flow of our trip booking process, which we’ll name `BookingSagaWorkflow`.
51+
52+
```php
53+
class BookingSagaWorkflow extends Workflow
54+
{
55+
public function execute()
56+
{
57+
}
58+
}
59+
```
60+
61+
Next, we’ll imbue our saga with logic, by adding booking steps:
62+
63+
```php
64+
class BookingSagaWorkflow extends Workflow
65+
{
66+
public function execute()
67+
{
68+
try {
69+
$flightId = yield ActivityStub::make(BookFlightActivity::class);
70+
$hotelId = yield ActivityStub::make(BookHotelActivity::class);
71+
$carId = yield ActivityStub::make(BookRentalCarActivity::class);
72+
} catch (Throwable $th) {
73+
}
74+
}
75+
}
76+
```
77+
78+
Everything inside the `try` block is our "happy path". If any steps within this distributed transaction fail, we move into the `catch` block and execute compensations.
79+
80+
Adding Compensations
81+
--------------------
82+
83+
```php
84+
class BookingSagaWorkflow extends Workflow
85+
{
86+
public function execute()
87+
{
88+
try {
89+
$flightId = yield ActivityStub::make(BookFlightActivity::class);
90+
$this->addCompensation(fn () => ActivityStub::make(CancelFlightActivity::class, $flightId));
91+
92+
$hotelId = yield ActivityStub::make(BookHotelActivity::class);
93+
$this->addCompensation(fn () => ActivityStub::make(CancelHotelActivity::class, $hotelId));
94+
95+
$carId = yield ActivityStub::make(BookRentalCarActivity::class);
96+
$this->addCompensation(fn () => ActivityStub::make(CancelRentalCarActivity::class, $carId));
97+
} catch (Throwable $th) {
98+
}
99+
}
100+
}
101+
```
102+
103+
In the above code, we sequentially book a flight, a hotel, and a car. We use the `$this->addCompensation()` method to add a compensation, providing a callable to reverse a distributed transaction.
104+
105+
Executing the Compensation Strategy
106+
-----------------------------------
107+
108+
With the above setup, we can finalize our saga and populate the `catch` block:
109+
110+
```php
111+
class BookingSagaWorkflow extends Workflow
112+
{
113+
public function execute()
114+
{
115+
try {
116+
$flightId = yield ActivityStub::make(BookFlightActivity::class);
117+
$this->addCompensation(fn () => ActivityStub::make(CancelFlightActivity::class, $flightId));
118+
119+
$hotelId = yield ActivityStub::make(BookHotelActivity::class);
120+
$this->addCompensation(fn () => ActivityStub::make(CancelHotelActivity::class, $hotelId));
121+
122+
$carId = yield ActivityStub::make(BookRentalCarActivity::class);
123+
$this->addCompensation(fn () => ActivityStub::make(CancelRentalCarActivity::class, $carId));
124+
} catch (Throwable $th) {
125+
yield from $this->compensate();
126+
throw $th;
127+
}
128+
}
129+
}
130+
```
131+
132+
Within the `catch` block, we call the `compensate()` method, which triggers the compensation strategy and executes all previously registered compensation callbacks. Once done, we rethrow the exception for debugging.
133+
134+
By default, compensations execute sequentially. To run them in parallel, use `$this->setParallelCompensation(true)`. To ignore exceptions that occur inside compensation activities while keeping them sequential, use `$this->setContinueWithError(true)` instead.
135+
136+
Testing the Workflow
137+
--------------------
138+
139+
Let’s run this workflow with simulated failures in each activity to fully understand the process.
140+
141+
First, we run the workflow normally to see the sequence of bookings: flight, then hotel, then rental car.
142+
143+
![booking saga with no errors](https://miro.medium.com/v2/1*3IgEjKzHK8Fpp-uumr4dIw.png)
144+
145+
Next, we simulate an error with the flight booking activity. Since no bookings were made, the workflow logs the exception and fails.
146+
147+
![booking saga error with flight](https://miro.medium.com/v2/1*ZuDAFa_q0l2-PT6PhRguaw.png)
148+
149+
Then, we simulate an error with the hotel booking activity. The flight is booked successfully, but when the hotel booking fails, the workflow cancels the flight.
150+
151+
![booking saga error with hotel](https://miro.medium.com/v2/1*_OwO5PUOLFqcLfd38gNpEQ.png)
152+
153+
Finally, we simulate an error with the rental car booking. The flight and hotel are booked successfully, but when the rental car booking fails, the workflow cancels the hotel first and then the flight.
154+
155+
![booking saga error with rental car](https://miro.medium.com/v2/1*3qR9GKQH-YtghwPK_x9wUQ.png)
156+
157+
Conclusion
158+
----------
159+
160+
In this tutorial, we implemented the Saga architecture pattern for distributed transactions in a microservices-based application using Laravel Workflow. Writing Sagas can be complex, but Laravel Workflow takes care of the difficult parts such as handling errors and retries, and invoking compensatory transactions, allowing us to focus on the details of our application.

0 commit comments

Comments
 (0)