Sage - Sagas implementation in pure elixir

Hi guys,

I want to show library I’m working on: Sage. It’s a dependency-free implementation of Sagas pattern in pure Elixir. It’s intended to simplify dealing with distributed transactions, especially with an error recovery/cleanup. Sage guarantees that either all the transactions in a saga are successfully completed or compensating transactions are run to amend a partial execution.

Elevator pitch is: “Sage is an Ecto.Multi for your business logic”.

With Sage you can do something like this:

import Sage

new()
|> run(:user, &create_user/2, &delete_user/3)
|> run(:plans, &fetch_subscription_plans/2)
|> run(:subscription, &create_subscription/2, &delete_subscription/3)
|> run_async(:delivery, &schedule_delivery/2, &delete_delivery_from_schedule/3)
|> run_async(:receipt, &send_email_receipt/2, &send_excuse_for_email_receipt/3)
|> run(:update_user, &set_plan_for_a_user/2, &rollback_plan_for_a_user/3)
|> finally(&acknowledge_job/2)

There are interesting papers I’ve used to get ideas for building it:

20 Likes

@AndrewDryga: You have a typo in your example.
Add & character before delete_subscription/3 at line: 6.
:slight_smile:

1 Like

This looks pretty interesting!

So at the moment I have various ‘services’ where each function either returns an Ecto.Multi or an Ecto.Queryable, and an API layer that combines all the different service functions using Repo.transaction/all etc so that I can do all the various things in a 'transaction.

This looks like it might make that easier, and allow integrating with other APIs in a nicer fashion.

Do you have any examples of this being used ‘in the wild’?

Unfortunately, I don’t have actual code samples how it looks in a real project, but Sage is built with very specific use cases in mind and I know places it fits well - I already was tired for a few times by writing and testing request cleanup logic for third-party services/microservices and heard that pain from a lot of colleagues.

There are many directions to grow it, I just need more confirmations and troughs from the community to pick the best one :).

Could you elaborate on that one? Sounds like remote closing a file handle to me. :smiley:

Sure! So for example we created a Stripe subscription while we were creating a user in our local database. This would be very simple example of a distributed transaction, where we synchronize state between local DB and remote DB and want state to be consistent after we finished the execution.

There can be three outcomes of a remote call: success, failure and unknown (we did not receive the response). When everything goes fine it’s still a simple HTTP call wrapped in DB transaction, but when something fails we need to make sure that we deleted effects on Stripe (deleted subscription) before rolling back the transaction.

Because Sage requires you to split transaction and compensation, and compensation match by transactions return - compensation by itself is easier to test (usually, smaller function you have - less complexity in the tests).

It’s worth to mention that Stripe for us is atomic database, internally they should clean up their effects when something fails. So if they did not close file socket - it’s their pain, we only need to amend effects we created.

I value mental model and semantics in Sage much more than underlying implementation.

You can read more in my blog post:
https://medium.com/nebo-15/introducing-sage-a-sagas-pattern-implementation-in-elixir-3ad499f236f6

4 Likes

Touched this recently, it’s pretty cool. :slight_smile:

1 Like

Hi Andrew,
Have you managed to make a sample project with Sage? like a little tutorial?