How business transactions helped decouple Rails controllers at TextMaster

Sergio Medina
TextMaster Engineering
5 min readDec 4, 2018
Photo by Amy Elting on Unsplash

Ruby on Rails is our beloved framework of choice here at TextMaster. It enables us to develop features fast and focus on what matters the most. However, if we don’t pay enough attention to the structure of our code we may end up with a well-known problem: fat controllers, controllers that do too many things, at too many layers, and become a nightmare to maintain.

In this blog post we will write a small Rails application with a controller that is well on its way of becoming fat. Through a series of refactorings we will split it into different classes achieving a more decoupled codebase that is easier to maintain and reason about.

Our app will be a simple ToDo list. We start off by creating a Task model with fields title (string), description (string), and completed (boolean). The TaskController may have an #update method that looks like this:

The controller is handling both business logic (updating the task) and user interactions (doing an HTTP redirect). This is a very simple example, in a real world application we would have a lot more going on, a few examples are:

  • input validation
  • check permissions: can the user update the task? should be their task!
  • various other things like sending notifications, update stats, etc.

In order to make our example a bit more realistic, let’s check that the user can actually edit the task:

Now the controller is doing even more things: dealing with HTTP, checking if the user is authorized to perform the action, and updating the task. We are coupling too much logic which makes our code difficult to reuse and test in isolation.

Moreover, note that updating the task and checking permissions is logic that is core to our business: the ToDo app. On the other hand, parsing input parameters and redirecting the user are not, this logic is related to the fact that we are exposing the functionality as a webapp (as opposed to a mobile app, an API of some sort, a desktop app, etc.)

Another concept that is commonly found in controllers and gets mixed up with business logic is form input validation (it was intentionally left out in this example for simplicity).

If we come back to this code in six months, how easily can we answer the question: what is the business action the update method is carrying out?

In this fairly small method the answer is easy to see, the method is updating some properties of a task. However, as our app grows, it becomes increasingly more difficult just by looking at any controller action to answer such question.

Last but not least, what if we need to expose this functionality via another channel? For instance a GraphQL API? We would need to copy and paste some of this code, but with care, as we would need to replace the HTTP-specific code with some other code specific to the new channel.

Refactoring: Take 1

Let’s pull the business logic out of the controller and create a class UpdateTask that is in charge of updating a task. We’ll refer to these classes as ServiceObjects as this is the most commonly used term but the name does not really matter, what matters is how we’ll split our code.

And we’ll also need to modify the controller’s action:

ActiveRecord’s #update method returns true if the record was successfully saved and false otherwise, and in the latter case it also populates the errors field with the validation errors (which will then be displayed in the :edit view). Our new code does a very similar thing, with the ServiceObject returning the task and the controller checking for errors with errors.any?.

This refactoring is a step forward because all our business logic is now encapsulated in the service object: this is the entry point of our business action “updating a task”, it sets a boundary between the core business logic, and how this logic is exposed/used.

If we needed to update a task from another part of our system, like a new GraphQL endpoint, we would simply call this service object, as the HTTP-specific code is no longer mixed up with the business logic.

Additionally, just by looking at the call to UpdateTask we instantly know what business action the “update” method is carrying out, it is clear.

This solves the problem of decoupling user interactions from domain changes, but we ended up with a “blob” of domain logic. This new problem is not exclusive of service objects, it’s a design issue, we jammed all the business logic into one place.

Another flaw of this approach is that we are using exceptions to control flow, which is generally seen as an anti-pattern.

If we take a closer look at our service object we can see it is violating the Single Responsibility Principle by doing two separate things: checking if a user can edit a task, and updating the task. Let’s extract the check into another object.

Refactoring: Take 2

First we extract the permissions check into its own class:

And we make the necessary modifications to the service object:

We did not need to change the controller action as it was simply calling UpdateTask.

After this second round of refactoring we ended up with a CheckPermissions class that is responsible for one and only one thing, it has a name that helps us understand it better, and it can be tested in isolation and reused.

The UpdateTask class is also easier to test, since we used dependency injection and CheckPermissions responds to #call we can now inject a proc using ->{true} or ->{false} in the UpdateTask spec to test the different scenarios.

After these two rounds of refactoring we successfully isolated our business logic, decoupling it from controller specific logic. We created business transactions that are now the sole entry-point of our application. Additionally we broke down these transactions into small components that are easier to reuse and easier to test.

The UpdateTask service object could have been written in many other ways, for instance we could raise on validation errors and catch the exception in the controller. The problem it needs to solve is to communicate to users multiple distinct error cases in a portable way, decoupled from presentation. The lack of a standardized approach to handle this problem can create inconsistencies and lead to developer confusion.

In part 2 of this post we’ll introduce dry-transaction. dry-transaction is a gem part of dry-rb which helps us standardize the modular approach to domain transactions while making error handling a primary concern.

--

--