How to use railway-oriented business transactions to unclutter your Rails controllers
When your Rails app needs to handle multiple steps, your controllers’ methods can become a mess.
Don’t despair, though. You can delegate sequential steps to business transactions and Marie-Kondo those messy controllers. I’ll show you how.
Let’s keep in mind that business transactions are different from ActiveRecord transactions. Business transactions allow you to create a series of steps, each resulting in a
Success or a
Failure object. ActiveRecord transactions are about ensuring that several database operations work as a single unit and are all rollbacked if any error occurs.
In this tutorial, I’ll use the dry-transaction gem whose documentation is neat.
From clean to messy controllers in no time
Let’s start with coding a basic controller.
Okay, so here’s a basic
LeadsController. You have two actions:
create. As you can see, the
create method is straightforward:
- I create a new lead with the information filled by our lead.
- I handle any errors by rendering the
leads/newform prefilled with the information our current lead already gave us.
- If everything goes well, I redirect our lead to her profile page (or any page of my choosing).
But what if I want to do more? Let’s say I want to:
- connect to a distant CRM through an API and synchronize the lead’s information with my sales team
- send a welcome text and an email to the lead
- notify someone in my team
As you can see, there are several new steps doing very different things. In a real-life app, we could do a lot more than that: create associations, generate SKU numbers, etc. The other thing is that all these steps are dependent on the
lead being created without any errors. This puts a lot of stuff into an
else branch. This is where business transactions can come in handy.
Let’s see how to do it.
Transactions to the rescue
Let’s set up dry-transaction and see how we can unclutter our controller.
Step 1: Install dry-transaction
Add this line to your application’s Gemfile:
Step 2: General principles
Before we dive into moving parts of our
LeadsController#create into a transaction, let’s look at a transaction file to see what’s what.
A business transaction is a series of operations where any can fail and stop the processing.
Each step is processed one at a time and must return either a
Success or a
Here are a few points to keep in mind:
teeis a specific kind of step. It’ll always return a
Success. This is fine here because I’m simply fetching an input.
stepis the basic operation. It’ll have to return either a
@paramswill be available to all other steps or methods of my transaction when
Step 3: Move the controller’s logic into the transaction
Now, we can move parts of our
LeadsController#create into a transaction.
My app’s architecture was:
At the end of this tutorial, it’ll be:
def self.call(*args, &block) will allow us to call the transaction from our controllers with a hash of arguments.
I’ll start with the transaction we started earlier and I’ll move parts of our
LeadsController#create into it.
All of my
LeadsController#create steps are now in my transaction.
Each operation handles its own errors and return a
Success or a
For instance, if my
MySmsProvider.welcome_sms(@lead).deliver_now returns an error, my transaction will not execute the next steps and will return a
Failure so I know that something went wrong here.
Step 4: Call the transaction and handle its results
Now that all my steps are in my transaction, what should I do with my controller? We’ll start by calling the transaction.
Calling a transaction will run its operations in their specified order, with the output of each operation becoming the input for the next.
As I said before, a transaction either returns a
Success or a
Failure object. I can handle these results in the controller.
In our original controller, I would render the
new form if the lead creation failed. On the other hand, if the creation succeeded, I’d redirect my new lead to its profile. Let’s do this now!
Now, my controller only handles calls to a grouped set of business operations. No more database operations mingling with sending out emails or redirection rules. There is some cohesiveness in the abstraction.
This is it!
Y’all go and checkout dry-transaction’s documentation and do not hesitate to read the source code for more magic!
Thank you Nicolas for your feedback and help with this post!
If you have any questions or if something is not clear enough, ping me on Twitter or create an issue on GitHub so we can make this tutorial better.
Next time, I’ll show you how to test your transactions with Rspec [update: it’s live! ✌️].