From Rails Controller to Business Action

Evgeniy Demin
5 min readApr 11

--

Since the beginning of Ruby on Rails, there have been no built-in solutions for organizing controllers and business logic. I witness engineers/companies still doing the same CRUD controllers that Rails scaffolding shows them to do.

Level up your controllers’ actions by extracting business logic.

While this might be a good approach in some cases, in my experience, most times, it should be done differently. Today, I would like to discuss this issue with you and what are the available solutions.

Let’s start by looking at an action you might have in your plain controller. I’ve made it based on the scaffolding template. I’ve also removed unnecessary parts to keep the focus on the subject.

class UsersController < ApplicationController
# ...

def update
if @user.update(user_params)
# render success
else
# render failure
end
end

private

def user_params
params.require(:user).permit(:email, :full_name)
end
end

As always, everything has its pros and cons. We can emphasize that this code is straightforward to read.

But what can be done better here?

Before jumping into that, let’s define a business action in software engineering.

Please take a moment to stop and guess how you would describe it before reading forward.

In software engineering, a “business action” typically refers to a unit of code that implements a specific business function or process. These actions are often designed to perform related tasks in a larger business workflow.

For example, let’s say you are building an e-commerce website. One of the business actions in your code might be to process a customer’s order. This action would involve several steps, such as checking the availability of products, calculating the total cost of the order, updating inventory levels, and generating an order confirmation for the customer.

Each step could be implemented as a separate function or method, and the overall business action would tie them together into a cohesive process. By breaking down the larger business workflow into smaller, more manageable code units, you can create more modular, reusable software that is easier to maintain and update over time.

Overall, designing business actions in software engineering aims to create code that reflects the underlying business logic and workflows, which can be easily adapted to business requirements or environment changes.

After familiarizing yourself, I believe it would be fair to say that the @user.update(...) call is an implicit business action. In this context, its purpose is to update the user’s email and full name.

Where is the promised issue, then?

I will describe several below.

Code isolation and reusability

In software development, it’s important to avoid duplicating business logic and instead extract it into reusable code units.

You probably have multiple places where you need to execute the same business logic. It could be data migrations, background jobs, or other APIs like GraphQL. While copying and pasting business logic into multiple places may seem convenient, this approach is prone to bugs. It can make it difficult to maintain and update the codebase over time.

Even if you don’t currently have other consumers for the business logic, extracting it into a separate code unit can help ensure consistency and avoid errors in the future.

Honestly, it isn’t the controller’s responsibility to handle business logic. Controllers are already having a lot of responsibilities. Extracting business logic from controllers can help improve the modularity and testability of your code by allowing you to focus on testing specific units of functionality in isolation.

Concrete business meaning

When designing software, it’s important to ensure that the codebase is organized in a way that reflects the concrete business meaning of the application.

It’s easy to lose the concrete business meaning of the action when you have generic, widely open update actions. I saw such actions growing in size to an enormous number of accepted attributes where it becomes hard to say what it does, how, and when it’s being used.

Let’s update the action to accept just eight fields.

def update_params
params.require(:user).permit(:email, :full_name, :address, :phone,
:balance, :default_currency,
:last_signed_at,
:last_order_id)

It’s only eight fields (I have seen 30+), but it’s hard to tell what it’s going on here for me.

Don’t get me wrong, it’s obvious that it just updates the user’s fields, but can you anticipate the flows/business actions?

If you look at the code above one more time, you can see that newlines split the list of attributes in a specific way. Continuing the example, this business action has four fully isolated consumers. For me, it’s a clue that an action should be split into four pieces representing their concrete meanings.

def update_personal_information
personal_information =
params.require(:user).permit(:email, :full_name, :address, :phone)

if @user.update(personal_information)
# ...
else
# ...
end
end

def update_billing
billing = params.require(:user).permit(:balance, :default_currency)

if @user.update(billing)
# ..
else
# ..
end
end

# and separate for :last_signed_at
# one for :last_order_id

The concrete-focused meaning is essential for understanding how the application generally behaves. It’s also much easier to extend its functionality. We are going to elaborate on this in the next bullet point.

Business functionality

Business functionality represents a list of things that business action does. So far, every business action above has described a single function — a user update.

Usually, it’s quite the opposite. In actual live applications, business actions consist of multiple steps. For example, I saw actions doing 20+ different things.

Let’s look at the instance where a user updates personal information. Probably, you don’t want to allow this change without sending a notification letter. So the resulting business action would look something like this:

def update_personal_information
personal_information =
params.require(:user).permit(:email, :full_name, :address, :phone)

if @user.update(personal_information)
# Send a notification letter to the user
NotificationLetter.to(user).send! # pseudo code
# ...
else
# ...
end
end

And the presented flows can grow tremendously. It’s crucial to support every step appropriately, considering its nature. They can be sync or async, revertable or not, critical or optional, etc.

I hope I provided enough reasons why putting the logic into the controller is a bad idea.

Please let me know if it didn’t convince you. I would be happy to discuss it further.

In summary, building a robust DSL for business actions can help solve the issues we’ve discussed, such as reusability, transparency, and scalability.

While several existing solutions are available, such as Granite, Operations, Trailblazer, and ActiveInteraction, you can always create your custom wrapper to control the required features.

Overall, I hope this article has emphasized treating your business logic as a valuable unit that deserves attention and care. By doing so, you’ll reap the benefits of a more efficient and maintainable codebase.

If you have any other alternatives to the ones mentioned here or would like to share your experience with these tools, please feel free to reach out.

Thank you for reading!

Please consider subscribing!

And don’t forget to share what you think about the topic. Just write me a message. I would be happy to discuss the topic or anything else with you!

--

--

Evgeniy Demin

Writing about Software Engineering and Self-Development. Stay tuned and grow your expertise. Let's connect on social media.