dry-transaction 0.10.0 brings class-based transactions and a whole new level of flexibility

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!