Programmers – This is how you manage complex workflows with ease

Anup Marwadi.NET, Development

In the world of software engineering, particularly in systems involving multiple subsystems and complex operations, ensuring transactional integrity and system reliability is paramount.

The Saga pattern emerges as a crucial architectural approach in handling these challenges, providing a structured method to manage distributed transactions without sacrificing scalability and reliability.

In this blog post, we’ll explore why the Saga pattern is essential for modern software systems, highlighting its alignment with SOLID principles, enhancing code readability, and simplifying the management of intricate business logic.

1. Facilitating Distributed Transactions Across Subsystems

Modern applications often involve multiple independent services or systems working in concert to perform complex operations. Traditional transactional models, like those based on two-phase commits, can hinder performance and scalability in distributed environments.

The Saga pattern addresses these limitations by breaking a global transaction into a series of local transactions, each managed and executed independently yet orchestrated to ensure the overall system’s consistency.

What’s important to know is that each step in a Saga represents a local transaction that can succeed or fail independently of others.

If a step fails, the Saga executes compensatory transactions to revert changes made by the preceding successful steps, thereby maintaining data integrity across subsystems.

This approach is crucial in systems where operations span multiple services, such as microservices architectures, where each service handles its transactions.

2. Adherence to SOLID Principles and Clean Code

Implementing the Saga pattern encourages developers to write clean and focused code.

In the Saga pattern, each step in the Saga is encapsulated as a separate, well-defined action that adheres to the Single Responsibility Principle, one of the core tenets of SOLID principles. This encapsulation ensures that each transactional step has one reason to change, simplifying maintenance and enhancing adaptability.

The Saga pattern utilizes Dependency Inversion and Interface Segregation principles by defining clear interfaces for each transaction step, promoting loose coupling between components.

Sagas help in building systems that are robust, easier to test, and flexible to changes by adhering to these principles.

Readable code is a cornerstone of successful software development. Code that is easy to read is inherently easier to maintain and debug, reducing the overall incidence of bugs

ANUP MARWADI, CEO – HYPERTRENDS GLOBAL INC.

3. Enhancing Code Readability and Maintainability

Readable code is a cornerstone of successful software development. Code that is easy to read is inherently easier to maintain and debug, reducing the overall incidence of bugs.

The Saga pattern enhances readability by structuring complex business operations into discrete steps, each represented clearly and concisely. This division allows developers to easily understand the workflow, identify the role of each transaction, and track how data transforms throughout the process.

For new team members or external reviewers, the clarity provided by the Saga pattern is invaluable. It allows them to decipher the business logic and data flow without needing to dig through convoluted code, facilitating quicker onboarding and effective collaboration.

4. Simplifying Complex Business Logic

Complex business operations often involve intricate logic and multiple conditional flows. The Saga pattern simplifies the management of these complexities by breaking down operations into manageable, independent steps. Each step handles specific parts of the business logic, maintaining isolation from others. This separation makes it easier to modify one part of the business process without affecting others, greatly reducing the risk of introducing errors into unrelated areas.

Moreover, the ability to restart or compensate for specific steps individually provides resilience and flexibility in handling failures. This is particularly beneficial in environments where operations are prone to interruptions or where certain steps might fail due to external dependencies.

Real-World Scenario – Gym Management Software – Membership Reinstatement

Nothing can better explain the Saga pattern better than a real-world scenario. We built a fitness/gym management B2B2C platform that allows gyms to better manage their customers, internal processes, and allows customers to manage their memberships, class registrations, payments etc.

We noticed that many times, customers will cancel their membership, and then request membership reinstatement months later. The sequence of events to reinstate a membership is as shown in the workflow below:

  1. Members select a new location and the membership package
  2. Members may decide to use a promo code that offers them discounts. For e.g. a gym location may want to send out a promo code to offer returning members a 10% perpetual discount.
  3. The system will check whether the Member’s membership is indeed canceled
  4. The system will perform various checks in Stripe to ensure that the subscription is canceled
  5. The system will ensure that the membership package being requested belongs to the location
  6. The system will allow the member to select an existing card, or add a new card
  7. If the member wants to add a new card, the system will create a new card in Stripe
  8. If the member requests a one-time package w/ fixed credits, the system performs an authorization charge in Stripe. Charge will not be captured until all critical steps are completed.
  9. If the member requests a monthly recurring billing subscription, the system will create a Subscription in Stripe
  10. The system will handle the Subscription created webhook and create a Subscription record
  11. The system will assign membership credits once the Subscription was successfully created
  12. If this was a one-time package, the system will now finally capture the transaction
  13. The system will create a successful payment record
  14. The system will create activity logs in a notification system so that the activity is tracked and available for administrators
  15. The system will reactivate the member record by setting certain flags and saving the preferred location (as the chosen location of the membership package)
  16. The system will send out an email confirmation to the member

This is a significantly complex workflow with multiple failure points. And that is where the Saga pattern and the division of labor with failure compensation kicks in. Each step above can be converted into Saga steps.

Here are our Saga Steps:

Saga Steps for a complex workflow

As shown below, a Saga Step accepts a global “context” that can be shared across multiple steps. This context allows one to track the saga progress and store state associated with the different saga steps.

public interface ISagaStep<TContext>
{
    Task ExecuteAsync(TContext context, CancellationToken cancellationToken = default);
    
    Task CompensateAsync(TContext context, CancellationToken cancellationToken = default);
    
}

The Saga can then be executed using a workflow as shown:

public class Saga : ISaga
{
    private readonly ILogger<Saga> _logger;

    public Saga(ILogger<Saga> logger)
    {
        _logger = logger;
    }
    
    public async Task<bool> ExecuteAsync<TContext>(IList<ISagaStep<TContext>> steps, TContext context)
    {
        var lastSuccessfulStep = -1;
        try
        {
            _logger.LogInformation("Executing saga");
            for (var i = 0; i < steps.Count; i++)
            {
                _logger.LogInformation("Executing step {StepIndex}", i);
                var step = steps[i];
                await step.ExecuteAsync(context);
                lastSuccessfulStep = i;
            }

            return true;
        }
        catch (Exception ex)
        {
            for (var i = lastSuccessfulStep; i >= 0; i--)
            {
                var step = steps[i];
                await step.CompensateAsync(context);
            }

            return false;
        }
        
    }
}

public interface ISaga
{
    /// <summary>
    /// Executes a series of steps in a saga.
    /// </summary>
    /// <param name="steps"></param>
    /// <param name="context"></param>
    /// <typeparam name="TContext"></typeparam>
    /// <returns></returns>
    Task<bool> ExecuteAsync<TContext>(IList<ISagaStep<TContext>> steps, TContext context);
}

In the above code, the system keeps a track of the execution status of each saga step. If a saga step fails, it will throw an exception. An exception will trigger the rollback process in reverse. The system will start from the last successful step and rollback everything.

The code snippet below demonstrates how that is done:

catch (Exception ex)
        {
            for (var i = lastSuccessfulStep; i >= 0; i--)
            {
                var step = steps[i];
                await step.CompensateAsync(context);
            }

            return false;
        }

Using the above steps, an extremely complex piece of code can be simplified as shown:

Saga Workflow Example

Conclusion

The Saga pattern is a powerful solution for managing distributed transactions in complex systems, promoting the development of clean, maintainable, and scalable software. By adhering to SOLID principles, enhancing readability, and simplifying complex business logic, Sagas help developers tackle modern software challenges effectively. As businesses continue to adopt distributed architectures, understanding and implementing the Saga pattern will be essential for any software developer aiming to build robust and reliable applications.