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
- Clean separation of concerns: Each operation becomes a standalone unit
- Reusability: Interactors can be reused across controllers, jobs, or other interactors
- Testability: Easy to test in isolation
- Error handling: Built-in error handling mechanism
- Code organization: A consistent way to structure complex operations
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:
- The current interactor marks the context as failed
- The Organizer immediately stops execution
- No subsequent interactors are called
- The context maintains the failure state and any error messages/data
- 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:
- Keep them focused: Each interactor should do exactly one thing
- Use organizers for complex flows: Combine simple interactors with organizers
- Test thoroughly: Interactors are easy to test in isolation
- Use meaningful names: Name interactors with verb phrases that describe the action
- Use context for communication: Pass all necessary data through the context
- Handle failures appropriately: Use
context.fail!
to signal errors - 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.