Ruby Interactor: Simplifying Business Logic in Your Applications

26 Apr 2025 - Gagan Shrestha

Ruby Interactor: Simplifying Business Logic in Your Applications

In the world of Ruby development, organizing business logic can quickly become challenging as applications grow in complexity. Enter Interactor - a simple yet powerful design pattern that can significantly improve how you structure your code.

What is Interactor?

Interactor is a Ruby gem that provides a common interface for performing complex business operations. At its core, an interactor represents a single unit of business logic, encapsulating all the steps required to execute that logic successfully.

The basic concept follows the Single Responsibility Principle - each interactor does exactly one thing. This makes your code more maintainable, testable, and easier to understand.

How Interactor Works

The typical implementation of an Interactor is straightforward:

1
2
3
4
5
6
7
8
9
10
11
12
13
class CreateUser
  include Interactor

  def call
    user = User.new(context.user_params)

    if user.save
      context.user = user
    else
      context.fail!(message: "Failed to create user: #{user.errors.full_messages.join(', ')}")
    end
  end
end

To use this interactor:

1
2
3
4
5
6
7
result = CreateUser.call(user_params: params[:user])

if result.success?
  # Use result.user
else
  # Handle the failure with result.message
end

Each interactor has access to a context object that stores input parameters and output values. If anything goes wrong, you can call context.fail! to indicate failure.

Interactor Benefits

Key Use Cases for Interactor

1. User Registration Flows

User registration often involves multiple steps: creating a user record, sending a welcome email, setting up default preferences, etc. An interactor organizes this nicely:

1
2
3
4
5
class RegisterUser
  include Interactor::Organizer

  organize [CreateUser, SendWelcomeEmail, SetupUserPreferences]
end

2. Payment Processing

Payment operations are perfect candidates for interactors because they typically involve multiple steps that should be organized carefully:

1
2
3
4
5
class ProcessPayment
  include Interactor::Organizer

  organize [ValidatePaymentDetails, ChargeCustomer, CreateReceipt, SendConfirmation]
end

3. Data Import/Export Operations

When importing CSV files or exporting data, several validations and transformations are often required:

1
2
3
4
5
class ImportUsersFromCsv
  include Interactor::Organizer

  organize [ValidateCsvFormat, ParseCsvData, CreateUserRecords, NotifyAdmin]
end

4. Multi-step Form Processing

Complex form submissions that update multiple records benefit greatly from interactors:

1
2
3
4
5
class ProcessApplicationForm
  include Interactor::Organizer

  organize [ValidateApplication, SaveApplicantDetails, ProcessDocuments, ScheduleInterview]
end

5. API Integrations

When working with third-party APIs, interactors help separate the integration logic:

1
2
3
4
5
class SyncWithExternalCrm
  include Interactor::Organizer

  organize [FetchLocalUsers, PrepareUserData, PushToExternalApi, LogSyncResults]
end

6. Transaction-like Operations

While Rails has database transactions, interactors can handle broader transactional behavior:

1
2
3
4
5
class CancelSubscription
  include Interactor::Organizer

  organize [ValidateCancellationEligibility, ProcessRefund, DeactivateSubscription, NotifyUser]
end

7. Background Jobs

For complex background processing tasks, interactors provide structure:

1
2
3
4
5
class GenerateMonthlyReport
  include Interactor::Organizer

  organize [GatherMetrics, CompileReportData, GeneratePdf, DeliverToStakeholders]
end

Handling Failures in Organizers

One of the most powerful aspects of Interactor is how it handles failures within an Organizer chain. When an Organizer is running through its sequence of interactors, if any individual interactor calls context.fail!, the entire chain is halted immediately. This provides a clean way to implement early-return behavior in complex processes.

Here’s what happens when a failure occurs:

  1. The current interactor marks the context as failed
  2. The Organizer immediately stops execution
  3. No subsequent interactors are called
  4. The context maintains the failure state and any error messages/data
  5. Control returns to the caller with the failed context

For example, in our user registration flow:

1
2
3
4
5
6
7
8
9
10
11
class SendWelcomeEmail
  include Interactor

  def call
    if email_service_available?
      # Send email logic
    else
      context.fail!(message: "Email service unavailable")
    end
  end
end

If SendWelcomeEmail fails, SetupUserPreferences will never be executed. The controller would receive a failed context:

1
2
3
4
5
6
7
8
result = RegisterUser.call(user_params: params[:user])

if result.success?
  redirect_to dashboard_path, notice: "Registration successful!"
else
  flash.now[:error] = result.message
  render :new
end

Custom Rollback Behavior

Interactors can define a rollback method that will be called if a later interactor in the chain fails:

1
2
3
4
5
6
7
8
9
10
11
12
class CreateUser
  include Interactor

  def call
    user = User.new(context.user_params)
    context.user = user.save ? user : context.fail!(message: "User creation failed")
  end

  def rollback
    context.user.destroy if context.user&.persisted?
  end
end

If SendWelcomeEmail fails, the rollback method of CreateUser will be called automatically, cleaning up the user record. This allows you to implement transactional-like behavior across multiple steps, even when they aren’t all database operations.

Best Practices

When working with interactors, consider these best practices:

  1. Keep them focused: Each interactor should do exactly one thing
  2. Use organizers for complex flows: Combine simple interactors with organizers
  3. Test thoroughly: Interactors are easy to test in isolation
  4. Use meaningful names: Name interactors with verb phrases that describe the action
  5. Use context for communication: Pass all necessary data through the context
  6. Handle failures appropriately: Use context.fail! to signal errors
  7. Implement rollbacks when needed: Define rollback methods for operations that need to be undone

Conclusion

Ruby Interactor provides an elegant solution for organizing business logic in your applications. By encapsulating operations into discrete, reusable units, you can create code that’s easier to understand, test, and maintain.

Whether you’re building a simple application or a complex system, adopting the Interactor pattern can help manage complexity and keep your codebase clean as it grows.

For Rails applications especially, interactors offer a refreshing alternative to bloated models and controllers, allowing you to extract and organize your business logic in a consistent, maintainable way.

Consider giving Interactor a try in your next Ruby project - your future self (and your team) will thank you for the clean, well-organized code.