0

I'm seeing two ways of orchestrating NServiceBus sagas. I'm not familiar with it so I'm trying to understand if they are just two alternative ways of doing the same thing or if I should follow one or the other.

The scenario is a saga that is going to perform three steps - each of which is some sort of complex behavior. As each one is completed the saga's state (Data) will be updated and then it will continue with the subsequent steps. (I'm trying to show just the absolute bare minimum.)

Option One

The first looks like this:

public class MySaga : Saga<MySagaData>,
    IAmStartedByMessages<DoStepOne>,
    IHandleMessages<DoStepTwo>,
    IHandleMessages<DoStepThree>
{
    protected override void ConfigureHowToFindSaga(SagaPropertyMapper<MySagaData> mapper)
    {
        mapper.MapSaga(saga => saga.SomeId)
            .ToMessage<DoStepOne>(msg => msg.SomeId)
            .ToMessage<DoStepTwo>(msg => msg.SomeId)
            .ToMessage<DoStepThree>(msg => msg.SomeId);
    }

    public async Task Handle(DoStepOne message, IMessageHandlerContext context)
    {
        var someComplexBehavior = new CommandToInvokeLogicForStepOne();
        await context.Send(someComplexBehavior);

        Data.StepOneComplete = true; // assume that this state is needed for some reason.

        var next = new DoStepTwo();
        await context.Send(next);
    }

    public async Task Handle(DoStepTwo message, IMessageHandlerContext context)
    {
        var someComplexBehavior = new CommandToInvokeLogicForStepTwo();
        await context.Send(someComplexBehavior);

        Data.StepTwoComplete = true;

        var next = new DoStepThree();
        await context.Send(next);
    }

    public async Task Handle(DoStepThree message, IMessageHandlerContext context)
    {
        var someComplexBehavior = new CommandToInvokeLogicForStepThree();
        await context.Send(someComplexBehavior);

        MarkAsComplete();
    }
}

Each step in the saga does something and then invokes the next step. From what I've seen, this is typical.

Option Two

The alternative is that the handlers for CommandToInvokeLogicForStepOne, CommandToInvokeLogicForStepTwo, and CommandToInvokeLogicForStepThree each invoke the next step in the saga.

For example, the handler for CommandToInvokeLogicForStepOne might look like this:

public class StepOneComplexBehaviorHandler : IHandleMessages<StepOneComplexBehaviorHandler>
{
    public async Task Handle(StepOneComplexBehaviorHandler message, IMessageHandlerContext context)
    {
        // do some complex logic

        var next = new StepOneDone();
        await context.Send(next); // <------ Next step in the saga
    }
}

And now the saga looks like this:

public class MySaga : Saga<MySagaData>,
    IAmStartedByMessages<DoStepOne>,
    IHandleMessages<StepOneDone>,
    IHandleMessages<StepTwoDone>,
    IHandleMessages<StepThreeDone>
{
    protected override void ConfigureHowToFindSaga(SagaPropertyMapper<MySagaData> mapper)
    {
        mapper.MapSaga(saga => saga.SomeId)
            .ToMessage<DoStepOne>(msg => msg.SomeId)
            .ToMessage<StepOneDone>(msg => msg.SomeId)
            .ToMessage<StepTwoDone>(msg => msg.SomeId);
    }

    public async Task Handle(DoStepOne message, IMessageHandlerContext context)
    {
        var someComplexBehavior = new CommandToInvokeLogicForStepOne();
        await context.Send(someComplexBehavior);
    }

    public async Task Handle(StepOneDone message, IMessageHandlerContext context)
    {
        Data.StepOneComplete = true;
        var someComplexBehavior = new CommandToInvokeLogicForStepTwo();
        await context.Send(someComplexBehavior);
    }

    public async Task Handle(StepTwoDone message, IMessageHandlerContext context)
    {
        Data.StepTwoComplete = true;
        var someComplexBehavior = new CommandToInvokeLogicForStepThree();
        await context.Send(someComplexBehavior);
    }

    public async Task Handle(StepThreeDone message, IMessageHandlerContext context)
    {
        MarkAsComplete();
    }
}

I find this less intuitive because if not for the obvious names (step one, step two) you can't look at any step and to tell which step is invoked next. It's necessary to look at each individual handler class and see what it invokes next. The saga does not specify its own orchestration.

That's just my opinion. Maybe it doesn't make any difference. But I'm hoping for some more experienced NServiceBus devs to share their input.

1 Answer 1

1

I've used both approaches in different scenarios and they both have advantages and disadvantages.

From a dogmatic design standpoint, Option 2 better follows the single responsibility principle, as in Option 1, Handle<DoStepOne> has two different reasons to change:

  1. The business logic of step one has changed
  2. You want to change what happens when step one is complete.

In Option 2, Handle<DoStepOne> only changes when the business logic of step one changes and Handle<StepOneDone> only change when you want to do something different when step one is complete. Option 2 also makes it easier to write unit tests for the saga coordination logic if your workflow isn't a simple linear process as the business logic of step 1 is decoupled from the logic of navigating between steps. For example, if you have a workflow where the result of step 1 determines if you go to step 2A or step 2B, this logic is then separated from the business logic of step 1.

From a more pragmatic standpoint, Option 1 has fewer lines of code, fewer messages, and less overall complexity for a simple example like this.

There are a number of factors to consider when choosing between the two options:

  • How tightly coupled are the steps in your process?
  • How likely are you to add new steps between the existing steps in your process?
  • Is there any conditional/branching logic as you move between steps?
  • How many steps do you have?

If you have a small number of tightly coupled steps with no conditional logic, Option 1 is simpler to implemented and may be easy to understand. If you have a large number of steps with a lot of conditional branching logic, using Option 2 and unit testing the conditional logic probably makes more sense. That being said, if you start with Option 1, you can always refactor to Option 2 in the future as the complexity of your workflow increases.

1
  • Thank you, this helps. Option two looks unusual to me, but I don't want to assign much weight to that because I lack experience. I'm not sure which is better in my case, but this at least tells me that option two isn't an aberration. That's enough for me to stop obsessing over it. yesterday

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.