Trigger saga instantaneously, without queue intervention

Say that an HTTP request comes in as the start of a logical series of events. For example, money is to be reserved from an account, to be eventually transferred elsewhere. The HTTP request expects a response, determined by whether we were able to reserve money and promise to eventually transfer it.

Now, the eventual stuff is likely to be processed in the background. It could use a SQL transport and not care about its polling delays. It enjoys the concurrency protection of sagas. All is good.

However, let’s consider the initial work. Let’s say that there is an existing saga that handles the account’s balance. As such, for us to provide a response to the HTTP request, that saga would need to handle a message.

It seems like a great waste (even more so with a SQL transport) to persist a message, have another endpoint instance pick it up after a delay (invoking the target saga), having it persist a reply, and then having the original HTTP request handler pick up the reply after another delay.

Instead, I would want the HTTP request handler to immediately handle the message that triggers the saga, i.e. not send it over any queue, but just instantiate and handle such a message instantaneously. This way, it can simply handle the HTTP request immediately, while stile benefitting from the concurrency protection of the saga.

Is this possible? Are there other ways to achieve a comparable effect?

(Note that it matters not if a failure occurs and the message is never handled: in the first step, we are still in HTTP territory. Even if we would persist and retry the input, the caller may already have experienced connection issues, with no clue of the result. The only guarantee that we can give with HTTP is that if the caller receives a success response, then we will [eventually] be consistent with our promise.)

1 Like

I think you’re looking for our Transactional Session feature.

It doesn’t involve the saga directly - that would be a somewhat dangerous antipattern in the first place. But it enables you to do some initial database work and also send a message atomically (either both succeed or both fail) so the rest of the backend stuff could continue, and that initial message could be the message that starts a saga.

Thanks @DavidBoike.

I think using a Transactional Session here still misses a very potent benefit. Simply doing the “regular” database interaction manually would put the responsibility for concurrency control back into the hands of the developer, whereas sagas take care of that for us. Sagas are far less error-prone, and easier to grasp, when it comes to concurrency control. That is why I would like to use a saga, not just further down the chain, but also at this point.

In essence, triggering the saga could be as simple as sending a message. However, that would introduce the overhead I described. I also just realized that it would introduce the risk that a queue full of unrelated work might cause the HTTP request to not get handled in time. For these reasons, I would want to handle the event instantaneously (and locally).

Is there an alternative way I could trigger the saga and benefit from its features?

Short answer: nope, you can’t.

Slightly longer answer: you’re hand-waving your way through a bunch of really complex stuff, with a ton of different different failure scenarios and no reliable queue to backstop it. I’m not really sure that what you’re imagining here is even possible.

That’s a pity. :confused:

What failure scenarios are we opening ourselves up to here that aren’t already and unavoidably in effect when we’re invoked over HTTP?

As for side effects, the saga being invoked will use the outbox pattern and a transaction, so our side effects are all-or-nothing, as usual.

As for staying in sync with the caller, the following is true regardless of whether we go through the transport or not, due to the nature of HTTP:

  • If the caller receives a 200 or 201 status code, they know for certain that we will [eventually] satisfy the request.
  • If the caller receives a 400-range status code, they know for certain that we will never satisfy the request.
  • In practically all other cases (including a 500 status code or a disconnect), the caller has no idea what is going on on our end. The request may or may not have been satisfied.

In fact, delayed retries are undesirable when we’re invoked over HTTP, since they make it more likely that the caller times out while we still satisfy the request, arguably the least favorable scenario.

Provided that the message is otherwise handled identically to how it would have been if it had come in through the remote transport (with saga locking and such), what newly introduced failure scenarios do you see?