Implementing a Serverless Saga Orchestrator

Thiago Custódio - Sep 13 '19 - - Dev Community

This article is part of #ServerlessSeptember. You'll find other helpful articles, detailed tutorials, and videos in this all-things-Serverless content collection. New articles are published every day — that's right, every day — from community members and cloud advocates in the month of September.

Find out more about how Microsoft Azure enables your Serverless functions at https://docs.microsoft.com/azure/azure-functions.

Introduction

No matter which tech event you attend, there will be someone talking about Microservices. It may not be for you at this moment but it's a fact that Microservices architecture advances loosely coupled services which can be developed, deployed and scaled independently. This architecture style also bring some challenges to the table and I'll address one of them in this article.

Saga Pattern

In order to ensure data consistency across multiple services, we often implement the Saga pattern: a sequence of local transactions. If a local transaction fails for some reason the Saga executes a series of compensating transactions that undo the changes that were made by the preceding local transactions.

This idea is not new, I've learned about it many years ago from my friend and professor Felipe Oliveira (@scaphe), but on that time it was applied to Services Oriented Architectures (SOA). There are two ways to implement the Saga Pattern:

  • Choreography: each local transaction publishes domain events that trigger the execution of transactions in other services;

  • Orchestration:the logic and order of execution lives on an orchestrator that commits / rollback all the phases related to that business logic;

Using Azure Functions in addition with Durable Functions, you can easily implement a serverless Saga Orchestrator (using FanOut and/or Function Chains):

Sample

In order to keep this example simple, I'll leave just the orchestrator logic in this article:

    public static class OrderSaga
    {
        [FunctionName("OrderSaga")]
        public static async Task<bool> RunOrchestrator(
            [OrchestrationTrigger] DurableOrchestrationContext context)
        {
            var orderId = context.GetInput<Guid>();

            Task<bool> orderResponse = context.CallActivityAsync<bool>("OrderActivity", orderId);
            Task<bool> paymentResponse = context.CallActivityAsync<bool>("PaymentActivity", orderId);

            await Task.WhenAll(orderResponse, paymentResponse);

            if (orderResponse.Result == false || paymentResponse.Result == false)
            {
                await context.CallActivityAsync("RollbackOrderActivity", orderId);
                await context.CallActivityAsync("RollbackPaymentActivity", orderId);

                return false;
            }

            return true;
        }

What is happening under the hood?

Durable functions expose an endpoint where we can monitor the progress and the output of each activity. It also persists the output from each activity and it has retry logic implemented by nature. All you have to do is implement the success / failure logics into your activities, then use the orchestrator to commit or rollback each phase, just what we've did previously.

More information

You can find more information in the following links:

. . . . . . . . . .