Improving Rails Code Maintainability with the Interactor Pattern

Reading Time: 4 minutes

Simplifying Code Complexity

As applications grow, developers often struggle with the increasing complexity of their codebase. What begins as a clean, modular project can quickly become unwieldy as new features are added. This complexity often leads to tightly coupled logic that is challenging to debug and even harder to extend.

This is where the Interactor pattern shines. Interactors break down complex logic into small, reusable components that are easy to understand, test, and maintain. By isolating business logic from controllers, interactors enhance code readability and reduce the potential for errors. This clear separation of responsibilities not only streamlines your development process but also simplifies future updates and additions to your codebase.

In this post, we’ll explore how implementing the Interactor pattern can help you simplify your code, improve maintainability, and adapt to the evolving needs of your application. Discover how this design pattern can make your development process more efficient and your codebase more robust.

Installation Steps: Getting Started with Interactors

Before diving into using Interactors in your Rails application, you’ll need to install the required gem and set up your project to make use of it.

  • Step 1: Add the Interactor Gem Start by adding the interactor gem to your Gemfile:

    gem "interactor", "~> 3.0"

    Then, run bundle install to install the gem:

    bundle install

    By adding the Interactor gem, you’re laying the groundwork for cleaner, more organized logic. It’s the first step toward transforming your controllers from cluttered behemoths into well-orchestrated workflows.

  • Step 2: Generate Interactors Interactors are plain Ruby classes, so you can create them manually. However, you can streamline the process by using a Rails generator. To get started, manually create interactors in theapp/interactors/ directory. If you want to use Rails generators, you may need to install the following gem:

    gem "interactor-rails", "~> 2.0"

    Once installed, you can generate an interactor as follows:

    rails generate interactor ProcessPayment

    This will generate a basic interactor:

    # app/interactors/process_payment.rb
    class ProcessPayment
      include Interactor
    
      def call
        # Your business logic goes here
      end
    end

Let’s say we have an OrdersController that is responsible for validating orders, processing payments, and sending confirmation emails.

class OrdersController < ApplicationController
  def create
    order = Order.new(order_params)

    unless order.valid?
      flash[:alert] = "Order is invalid"
      render :new and return
    end

    if PaymentGateway.charge(order)
      order.update(status: 'paid')
    else
      flash[:alert] = "Payment failed"
      render :new and return
    end

    begin
      UserMailer.order_confirmation(order).deliver_now
    rescue => e
    flash[:alert] = "Failed to send confirmation email: #{e.message}"
      redirect_to order_path(order) and return
    end

    # Success: Redirect to order confirmation page
    redirect_to order_path(order), notice: "Order successfully created!"
  end

  private

  def order_params
    params.require(:order).permit(:customer_id, :total_price, :items)
  end
end

This code can quickly become a headache as the app scales. The solution? Split the logic into three interactors: ValidateOrder, ProcessPayment, and SendOrderConfirmation. Each interactor focuses on one task, making the code easier to manage, test, and extend.

class ValidateOrder
  include Interactor

  def call
    order = context.order
    unless order.valid?
      context.fail!(message: "Order is invalid")
    end
  end
end

class ProcessPayment
  include Interactor

  def call
    order = context.order
    if PaymentGateway.charge(order)
      context.order.update(status: 'paid')
    else
      context.fail!(message: "Payment failed")
    end
  end
end

class SendOrderConfirmation
  include Interactor

  def call
    UserMailer.order_confirmation(context.order).deliver_now
  rescue => e
    context.fail!(message: "Failed to send confirmation email: #{e.message}")
  end
end

By splitting these responsibilities, each interactor has a singular focus. This not only simplifies debugging but also encourages reusability, making your codebase flexible to future changes.

What is an Organizer?

Now that we’ve decomposed the logic into smaller, manageable pieces, we need an effective way to orchestrate them. Imagine the organizer as the conductor of an orchestra, where each interactor plays its unique part in harmony. The organizer ensures that each component executes in the right order, gracefully managing any errors that may arise along the way. This orchestration not only streamlines the process but also enhances the overall functionality and reliability of the application.

Here’s how we can create an organizer that ties everything together:

class CreateOrderOrganizer
  include Interactor::Organizer

# Define the order in which interactors should run, here the order is important.
  organize ValidateOrder,
                ProcessPayment, 
                 SendOrderConfirmation
end

The organizer coordinates the workflow, ensuring each interactor runs in sequence. Now, you can refactor the OrdersController to use this new structure:

class OrdersController < ApplicationController
  def create
    order = Order.new(order_params)

    result = CreateOrderOrganizer.call(order: order)

    if result.success?
      redirect_to order_path(order), notice: "Order successfully created!"
    else
      render :new, alert: result.message
    end
  end

  private

  def order_params
    params.require(:order).permit(:customer_id, :total_price, :items)
  end
end

By letting the organizer handle the business logic, the controller becomes cleaner and more focused on what it’s supposed to do—handle the request and return a response.

Conclusion

Interactors decompose complex logic into isolated, focused components, significantly improving the clarity and maintainability of your codebase. With interactors, you can say goodbye to bulky controllers and tangled business logic. Each interactor is designed to perform a single function effectively, ensuring that your logic remains simple, reusable, and easy to test.

Organizers allow you to stitch together multiple interactors into cohesive workflows, handling both success and failure scenarios. This structured approach keeps your controllers clean and ensures that your business logic is handled consistently across your application.

Want to see the impact of Interactors firsthand? Start refactoring a controller today, or explore the Interactor GitHub and Interactor-Rails GitHub pages to dive deeper into this pattern. You’ll quickly discover how this small change can have a big effect on your Rails codebase.

Bring in a Professional!

Maintaining a clean and scalable Rails codebase can be challenging, but we’re here to simplify the process! Our expert team specializes in implementing the Interactor pattern to optimize your business logic and enhance code maintainability. Whether you need to refactor existing code or build a scalable application from the ground up, our skilled developers are ready to provide reliable and efficient solutions tailored to your needs.

Hire an expert from MagmaLabs today and discover the advantages of cleaner, more maintainable code!

0 Shares:
You May Also Like
Read More

Setting Up EasyPost on Solidus

Reading Time: 5 minutesSolidus provides a flexible system to calculate shipping by accommodating a wide range of shipment pricing: from simple flat…