Skip to main content

2 posts tagged with "microservices"

View All Tags

· 4 min read
Richard

captionless image

In the evolving landscape of microservices, communication has always been a focal point. Microservices can interact in various ways, be it through HTTP/REST calls, using messaging protocols like RabbitMQ or Kafka, or even employing more recent technologies like gRPC. Yet, regardless of the communication method, the goal remains the same: seamless, efficient, and robust interactions. Today, we’ll explore how Laravel Workflow can fit into this picture and optimize the communication between microservices in a unique way.

The Challenge

In a microservices architecture, decoupling is the name of the game. You want each service to have a single responsibility, to be maintainable, and to be independently deployable. Yet, in the world of workflows, this becomes challenging. How do you split a workflow from its activity and yet ensure they communicate seamlessly?

Laravel Workflow to the Rescue!

Laravel Workflow handles the discovery and orchestration for you! With a shared database and queue connection, you can have your workflow in one Laravel app and its activity logic in another.

Defining Workflows and Activities

1. Create a workflow.

use Workflow\ActivityStub;
use Workflow\Workflow;

class MyWorkflow extends Workflow
{
public function execute($name)
{
$result = yield ActivityStub::make(MyActivity::class, $name);
return $result;
}
}

2. Create an activity.

use Workflow\Activity;

class MyActivity extends Activity
{
public function execute($name)
{
return "Hello, {$name}!";
}
}

3. Run the workflow.

use Workflow\WorkflowStub;

$workflow = WorkflowStub::make(MyWorkflow::class);
$workflow->start('world');
while ($workflow->running());
$workflow->output();
// Output: 'Hello, world!'

The workflow will manage the activity and handle any failures, retries, etc. Think of workflows like job chaining on steroids because you can have conditional logic, loops, return a result that can be used in the next activity, and write everything in typical PHP code that is failure tolerant.

Balancing Shared and Dedicated Resources

When working with microservices, it’s common for each service to have its dedicated resources, such as databases, caches, and queues. However, to facilitate communication between workflows and activities across services, a shared connection (like a database or queue) becomes essential. This shared connection acts as a bridge for data and task exchanges while ensuring:

  1. Isolation: Dedicated resources prevent cascading failures.
  2. Performance: Each service can be optimized independently.
  3. Security: Isolation limits potential attack vectors.

Step-By-Step Integration

1. Install laravel-workflow in all microservices.

Follow the installation guide.

2. Create a shared database/redis connection in all microservices.

// config/database.php
'connections' => [
'shared' => [
'driver' => 'mysql',
'host' => env('SHARED_DB_HOST', '127.0.0.1'),
'database' => env('SHARED_DB_DATABASE', 'forge'),
'username' => env('SHARED_DB_USERNAME', 'forge'),
'password' => env('SHARED_DB_PASSWORD', ''),
],
],

3. Configure a shared queue connection.

// config/queue.php
'connections' => [
'shared' => [
'driver' => 'redis',
'connection' => 'shared',
'queue' => env('SHARED_REDIS_QUEUE', 'default'),
],
],

4. Ensure only one microservice publishes Laravel Workflow migrations.

Update the migration to use the shared database connection.

// database/migrations/..._create_workflows_table.php
class CreateWorkflowsTable extends Migration
{
protected $connection = 'shared';
}

5. Extend workflow models in each microservice to use the shared connection.

// app/Models/StoredWorkflow.php
namespace App\Models;

use Workflow\Models\StoredWorkflow as BaseStoredWorkflow;

class StoredWorkflow extends BaseStoredWorkflow
{
protected $connection = 'shared';
}

6. Publish Laravel Workflow config and update it with shared models.

php artisan vendor:publish --provider="Workflow\Providers\WorkflowServiceProvider" --tag="config"

7. Set workflows and activities to use the shared queue.

// app/Workflows/MyWorkflow.php
class MyWorkflow extends Workflow
{
public $connection = 'shared';
public $queue = 'workflow';
}
// app/Workflows/MyActivity.php
class MyActivity extends Activity
{
public $connection = 'shared';
public $queue = 'activity';
}

8. Ensure microservices define empty counterparts for workflow and activity classes.

In the workflow microservice:

class MyWorkflow extends Workflow
{
public $connection = 'shared';
public $queue = 'workflow';

public function execute($name)
{
yield ActivityStub::make(MyActivity::class, $name);
}
}
class MyActivity extends Activity
{
public $connection = 'shared';
public $queue = 'activity';
}

In the activity microservice:

class MyWorkflow extends Workflow
{
public $connection = 'shared';
public $queue = 'workflow';
}
class MyActivity extends Activity
{
public $connection = 'shared';
public $queue = 'activity';

public function execute($name)
{
return "Hello, {$name}!";
}
}

9. Ensure all microservices have the same APP_KEY in their .env file.

This is crucial for proper job serialization across services.

10. Run queue workers in each microservice.

php artisan queue:work shared --queue=workflow
php artisan queue:work shared --queue=activity

Conclusion

By following the steps above, you can ensure seamless interactions between microservices while maintaining modularity and scalability. Laravel Workflow takes care of the discovery and orchestration for you. 🚀

Thanks for reading!

· 5 min read
Richard

Suppose we are working on a Laravel application that offers trip booking. A typical trip booking involves several steps such as:

  1. Booking a flight.
  2. Booking a hotel.
  3. Booking a rental car.

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.

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.

Prerequisites

To follow this tutorial, you should:

  1. Set up a local development environment for Laravel Workflow applications in PHP or use the sample app in a GitHub codespace.
  2. Familiarize yourself with the basics of starting a Laravel Workflow project by reviewing the documentation.
  3. Review the Saga architecture pattern.

Sagas are an established design pattern for managing complex, long-running operations:

  1. A Saga manages transactions using a sequence of local transactions.
  2. A local transaction is a work unit performed by a saga participant (a microservice).
  3. Each operation in the Saga can be reversed by a compensatory transaction.
  4. The Saga pattern assures that all operations are either completed successfully or the corresponding compensation transactions are run to reverse any completed work.

Laravel Workflow provides inherent support for the Saga pattern, simplifying the process of handling rollbacks and executing compensatory transactions.

Booking Saga Flow

We will visualize the Saga pattern for our trip booking scenario with a diagram.

trip booking saga

Workflow Implementation

We’ll begin by creating a high-level flow of our trip booking process, which we’ll name BookingSagaWorkflow.

class BookingSagaWorkflow extends Workflow  
{
public function execute()
{
}
}

Next, we’ll imbue our saga with logic, by adding booking steps:

class BookingSagaWorkflow extends Workflow  
{
public function execute()
{
try {
$flightId = yield ActivityStub::make(BookFlightActivity::class);
$hotelId = yield ActivityStub::make(BookHotelActivity::class);
$carId = yield ActivityStub::make(BookRentalCarActivity::class);
} catch (Throwable $th) {
}
}
}

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.

Adding Compensations

class BookingSagaWorkflow extends Workflow  
{
public function execute()
{
try {
$flightId = yield ActivityStub::make(BookFlightActivity::class);
$this->addCompensation(fn () => ActivityStub::make(CancelFlightActivity::class, $flightId));

$hotelId = yield ActivityStub::make(BookHotelActivity::class);
$this->addCompensation(fn () => ActivityStub::make(CancelHotelActivity::class, $hotelId));

$carId = yield ActivityStub::make(BookRentalCarActivity::class);
$this->addCompensation(fn () => ActivityStub::make(CancelRentalCarActivity::class, $carId));
} catch (Throwable $th) {
}
}
}

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.

Executing the Compensation Strategy

With the above setup, we can finalize our saga and populate the catch block:

class BookingSagaWorkflow extends Workflow  
{
public function execute()
{
try {
$flightId = yield ActivityStub::make(BookFlightActivity::class);
$this->addCompensation(fn () => ActivityStub::make(CancelFlightActivity::class, $flightId));

$hotelId = yield ActivityStub::make(BookHotelActivity::class);
$this->addCompensation(fn () => ActivityStub::make(CancelHotelActivity::class, $hotelId));

$carId = yield ActivityStub::make(BookRentalCarActivity::class);
$this->addCompensation(fn () => ActivityStub::make(CancelRentalCarActivity::class, $carId));
} catch (Throwable $th) {
yield from $this->compensate();
throw $th;
}
}
}

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.

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.

Testing the Workflow

Let’s run this workflow with simulated failures in each activity to fully understand the process.

First, we run the workflow normally to see the sequence of bookings: flight, then hotel, then rental car.

booking saga with no errors

Next, we simulate an error with the flight booking activity. Since no bookings were made, the workflow logs the exception and fails.

booking saga error with flight

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.

booking saga error with hotel

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.

booking saga error with rental car

Conclusion

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.