Interactor
Interactor provides a common interface for performing complex user interactions.
Install / Use
/learn @collectiveidea/InteractorREADME
Interactor
Getting Started
Add Interactor to your Gemfile and bundle install.
gem "interactor", "~> 3.0"
What is an Interactor?
An interactor is a simple, single-purpose object.
Interactors are used to encapsulate your application's business logic. Each interactor represents one thing that your application does.
Context
An interactor is given a context. The context contains everything the interactor needs to do its work.
When an interactor does its single purpose, it affects its given context.
Adding to the Context
As an interactor runs it can add information to the context.
context.user = user
Failing the Context
When something goes wrong in your interactor, you can flag the context as failed.
context.fail!
When given a hash argument, the fail! method can also update the context. The
following are equivalent:
context.error = "Boom!"
context.fail!
context.fail!(error: "Boom!")
You can ask a context if it's a failure:
context.failure? # => false
context.fail!
context.failure? # => true
or if it's a success.
context.success? # => true
context.fail!
context.success? # => false
Dealing with Failure
context.fail! always throws an exception of type Interactor::Failure.
Normally, however, these exceptions are not seen. In the recommended usage, the controller invokes the interactor using the class method call, then checks the success? method of the context.
This works because the call class method swallows Interactor::Failure exceptions. When unit testing an interactor, if calling custom business logic methods directly and bypassing call, be aware that fail! will generate such exceptions.
See Interactors in the Controller, below, for the recommended usage of call and success?.
Hooks
Before Hooks
Sometimes an interactor needs to prepare its context before the interactor is even run. This can be done with before hooks on the interactor.
before do
context.emails_sent = 0
end
A symbol argument can also be given, rather than a block.
before :zero_emails_sent
def zero_emails_sent
context.emails_sent = 0
end
After Hooks
Interactors can also perform teardown operations after the interactor instance is run.
after do
context.user.reload
end
NB: After hooks are only run on success. If the fail! method is called, the interactor's after hooks are not run.
Around Hooks
You can also define around hooks in the same way as before or after hooks, using
either a block or a symbol method name. The difference is that an around block
or method accepts a single argument. Invoking the call method on that argument
will continue invocation of the interactor. For example, with a block:
around do |interactor|
context.start_time = Time.now
interactor.call
context.finish_time = Time.now
end
With a method:
around :time_execution
def time_execution(interactor)
context.start_time = Time.now
interactor.call
context.finish_time = Time.now
end
NB: If the fail! method is called, all of the interactor's around hooks cease execution, and no code after interactor.call will be run.
Hook Sequence
Before hooks are invoked in the order in which they were defined while after hooks are invoked in the opposite order. Around hooks are invoked outside of any defined before and after hooks. For example:
around do |interactor|
puts "around before 1"
interactor.call
puts "around after 1"
end
around do |interactor|
puts "around before 2"
interactor.call
puts "around after 2"
end
before do
puts "before 1"
end
before do
puts "before 2"
end
after do
puts "after 1"
end
after do
puts "after 2"
end
will output:
around before 1
around before 2
before 1
before 2
after 2
after 1
around after 2
around after 1
Interactor Concerns
An interactor can define multiple before/after hooks, allowing common hooks to be extracted into interactor concerns.
module InteractorTimer
extend ActiveSupport::Concern
included do
around do |interactor|
context.start_time = Time.now
interactor.call
context.finish_time = Time.now
end
end
end
An Example Interactor
Your application could use an interactor to authenticate a user.
class AuthenticateUser
include Interactor
def call
if user = User.authenticate(context.email, context.password)
context.user = user
context.token = user.secret_token
else
context.fail!(message: "authenticate_user.failure")
end
end
end
To define an interactor, simply create a class that includes the Interactor
module and give it a call instance method. The interactor can access its
context from within call.
Interactors in the Controller
Most of the time, your application will use its interactors from its controllers. The following controller:
class SessionsController < ApplicationController
def create
if user = User.authenticate(session_params[:email], session_params[:password])
session[:user_token] = user.secret_token
redirect_to user
else
flash.now[:message] = "Please try again."
render :new, status: :unprocessable_entity
end
end
private
def session_params
params.require(:session).permit(:email, :password)
end
end
can be refactored to:
class SessionsController < ApplicationController
def create
result = AuthenticateUser.call(session_params)
if result.success?
session[:user_token] = result.token
redirect_to result.user
else
flash.now[:message] = t(result.message)
render :new, status: :unprocessable_entity
end
end
private
def session_params
params.require(:session).permit(:email, :password)
end
end
The call class method is the proper way to invoke an interactor. The hash
argument is converted to the interactor instance's context. The call instance
method is invoked along with any hooks that the interactor might define.
Finally, the context (along with any changes made to it) is returned.
When to Use an Interactor
Given the user authentication example, your controller may look like:
class SessionsController < ApplicationController
def create
result = AuthenticateUser.call(session_params)
if result.success?
session[:user_token] = result.token
redirect_to result.user
else
flash.now[:message] = t(result.message)
render :new
end
end
private
def session_params
params.require(:session).permit(:email, :password)
end
end
For such a simple use case, using an interactor can actually require more code. So why use an interactor?
Clarity
We often use interactors right off the bat for all
of our destructive actions (POST, PUT and DELETE requests) and since we
put our interactors in app/interactors, a glance at that directory gives any
developer a quick understanding of everything the application does.
▾ app/
▸ controllers/
▸ helpers/
▾ interactors/
authenticate_user.rb
cancel_account.rb
publish_post.rb
register_user.rb
remove_post.rb
▸ mailers/
▸ models/
▸ views/
TIP: Name your interactors after your business logic, not your
implementation. CancelAccount will serve you better than DestroyUser as the
account cancellation interaction takes on more responsibility in the future.
The Future™
SPOILER ALERT: Your use case won't stay so simple.
In our experience, a simple task like authenticating a user will eventually take on multiple responsibilities:
- Welcoming back a user who hadn't logged in for a while
- Prompting a user to update his or her password
- Locking out a user in the case of too many failed attempts
- Sending the lock-out email notification
The list goes on, and as that list grows, so does your controller. This is how fat controllers are born.
If instead you use an interactor right away, as responsibilities are added, your controller (and its tests) change very little or not at all. Choosing the right kind of interactor can also prevent simply shifting those added responsibilities to the interactor.
Kinds of Interactors
There are two kinds of interactors built into the Interactor library: basic interactors and organizers.
Interactors
A basic interactor is a class that includes Interactor and defines call.
class AuthenticateUser
include Interactor
def call
if user = User.authenticate(context.email, context.password)
context.user = user
context.token = user.secret_token
else
context.fail!(message: "authenticate_user.failure")
end
end
end
Basic interactors are the building blocks. They are your application's single-purpose units of work.
Organizers
An organizer is an important variation on the basic interactor. Its single purpose is to run other interactors.
class PlaceOrder
include Interactor::Organizer
organize CreateOr
Related Skills
node-connect
349.2kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
109.5kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
openai-whisper-api
349.2kTranscribe audio via OpenAI Audio Transcriptions API (Whisper).
qqbot-media
349.2kQQBot 富媒体收发能力。使用 <qqmedia> 标签,系统根据文件扩展名自动识别类型(图片/语音/视频/文件)。
