We're thrilled to announce the release of dry-transaction 0.10.0, which offers a huge improvement in ease-of-use and flexibility around designing your application's business transactions.

dry-transaction has been around for long enough now that it's really been put through its paces across many different apps and use cases. We'd begun to notice one big deficiency in its design: apart from defining the steps, we couldn't customize any other aspect of transaction behavior.

This all changes with dry-transaction 0.10.0 and the introduction of class-based transactions. Instead of defining a transaction in a special DSL block, you can now define it within your own class:

class MyTransaction include Dry :: Transaction ( container: MyContainer ) step :one , with: "operations.one" step :two , with: "operations.two" end my_trans = MyTransaction . new my_trans . ( some_input )

Transactions may resolve their operations from containers as before, but they can also now work entirely with local methods ("look ma, no container!"):

class MyTransaction include Dry :: Transaction step :one step :two def one ( input ) Right ( do_something ( input )) end def two ( input ) Right ( do_another_thing ( input )) end end

This isn't an either/or proposition. You can mix steps using instance methods and container operations:

class MyTransaction include Dry :: Transaction ( container: MyContainer ) step :one , with: "operations.one" step :local step :two , with: "operations.two" def local ( input ) # Do something between steps one and two Right ( input ) end end my_trans = MyTransaction . new

We can also use local methods to wrap external operations and provide some custom behaviour that is specific to their particular transaction. For example, this would be useful if you need to massage the input/output arguments to suit the requirements of individual operations.

class MyTransaction include Dry :: Transaction ( container: MyContainer ) step :one , with: "operations.one" step :two , with: "operations.two" def two ( input ) adjusted_input = do_something_with ( input ) # Call super to run the original operation super ( adjusted_input ) end end

Of course, this is just one example. We can't pretend to know everything you might do here, but what's exciting is that anything is now possible!

Another benefit of building transactions into classes is that we can now inject alternative step operations via the initializer. This allows you to modify the behavior of your transactions at runtime, and would be especially helpful for testing, since you can supply test doubles to simulate various different conditions.

class MyTransaction include Dry :: Transaction ( container: MyContainer ) step :one , with: "operations.one" step :two , with: "operations.two" end my_trans = MyTransaction . new ( one: alternative_operation_for_one )

Now that our transaction builder is a module, we can much more naturally provide common behavior across multiple transactions, like be defining a reusable module for a particular configuration:

module MyApp Transaction = Dry :: Transaction ( container: MyContainer ) class MyTransaction include MyApp :: Transaction step :one , with: "operations.one" step :two , with: "operations.two" end

Or even by building a base class for defining additional, common transaction behavior:

module MyApp class Transaction self . inherited ( klass ) klass . send :include , Dry :: Transaction ( container: MyContainer ) end def call ( input ) # Provide custom behaviour for calling transactions super ( input ) end # Or add common methods for all your transactions here end end class MyTransaction < MyApp :: Transaction step :one , with: "operations.one" step :two , with: "operations.two" end

This release wouldn't have happened without the efforts of Gustavo Caso, our newly-minted dry-rb core team member. Gracias, Gustavo 🙏🏻

We're really excited to see what you can do with the new dry-transaction. Please give it a try and share your experiences with us!