dry-transaction is dry-rb's answer to modelling complex business transactions in your applications. With dry-transaction, you can arrange multiple processing operations into a linear pipeline, with the output of each step becoming the input of the next. dry-transaction also elevates error handling to a first-class concern. Any error will halt the flow of operations, and you have powerful APIs for matching and acting on errors.
dry-transaction 0.6.0 is out today, and it brings a powerful new feature to your application's business transactions: support for defining your own custom step adapters. This allows you to encode powerful application-specific behaviours into steps that can be invoked with just a single word inside your transaction definitions.
Let's see this in action with a common sort of transaction:
publish_article = Dry.Transaction(container: MyApp::Container) do step :publish, with: "admin.articles.operations.publish_article" step :index, with: "search.operations.index_article" step :generate_pdf, with: "admin.articles.operations.generate_pdf" step :notify_contributors, with: "admin.articles.operations.notify_contributors" end
Here we have a series of operations we want to run whenever we publish an article. The beauty of managing them via a transaction here is that it's easy to see what will run, and each step is kept loosely coupled, with each only expecting an article object as input.
However, you might notice some of these steps look like they could be quite slow-running, namely
notify_contributors. We won't want to keep the user waiting for these steps to run before their browser request completes.
These are exactly the kind of steps we would want to push into a queue of background jobs. Here's where a custom step adapter comes in handy. Let's put one together:
require "kleisli" require "admin/import" module Admin class TransactionStepAdapters < Dry::Transaction::StepAdapters class Enqueue include Admin::Import("admin.enqueue_background_job") def call(step, *args, input) enqueue_background_job.(step.operation_name, *args, input) Right(input) end end register :enqueue, Enqueue.new end end
As you can see, dry-transaction step adapters only need to implement a single method:
#call(step, *args, input). In this case, we get the step's
operation_name (its identifier within the container), and pass that name along with the step's input arguments to the
enqueue_background_job object, whose purpose is to push that operation into the background queue. I won't go into detail about this object in this article, since it will be specific to your app and its queueing system.
Once we have this adapter in place, the next thing we need to do is make sure it's available to our transactions. You can see above that we've done this by making our own
TransactionStepAdapters container that inherits from
Dry::Transaction::StepAdapters (which means we keep dry-transaction's default adapters), and then registering our
enqueue adapter with it.
Now we can pass the container to our transaction and start using the new adapter:
publish_article = Dry.Transaction(container: MyApp::Container, step_adapters: Admin::TransactionStepAdapters) do step :publish, with: "admin.articles.operations.publish_article" step :index, with: "search.operations.index_article" enqueue :generate_pdf, with: "admin.articles.operations.generate_pdf" enqueue :notify_contributors, "admin.articles.operations.notify_contributors" end
And there we have it! With the new infrastructure in place, we could change just two words in our transaction and have long-running jobs pushed into the background queue, while keeping everything just as easy to understand in a single glance.