I've spent a few months now using the interactor gem, both in my day job and also quite extensively in GitArborist‘s code. My first time hearing about “interactors” was from a talk by Robert Martin on clean architecture, essentially saying that an “interactor” object should encapsulate a single “use case” or user story, basically the business logic. Therefore, if you look at a list of interactors in the system you can quickly determine what operations that system carries out (in theory).

“Service Objects”

In some circles, these kinds of “business logic” objects are called Service Objects. They typically have a single public method (e.g. call or execute), sometimes even with a simple class-level method to simplify their usage. Generally, these objects have no state and are essentially procedural code wrapped in Object-Oriented trimmings.

class DoTheThing
  def self.call(*args)
    new.call(*args)
  end
  
  def call(a, b)
    a + b
  end
end

DoTheThing.call(1, 2)
=> 3

The idea here is to break the code into small, isolated, pieces that can be easily understood and tested in isolation.

The Interactor Gem

There are various “Service Object” libraries, or you can simply roll-your-own using something like the snippet above. The interactor gem has some niceties it provides out of the box. Firstly, every interactor provides a call method, which is (typically) the only public method. The gem wraps this for you so calling an interactor becomes InteractorClass.call(...), similar to the snippet above.

The return value, however, is a “context” object provided by the gem itself.

{{ <midroll_ad> }}

The context Object

The context has success? and failure? helpers for you to determine what path to take after calling the interactor. If an exception occurs this is automatically caught and results in a failed context being returned. Failure can also be set manually within an interactor by calling context.fail!(...).

Beyond that, the fields on context are whatever you want them to be. Anything passed into call becomes a field on the context, and you can assign new entries on-the-fly via context.new_context_field = 123 in your interactor.

Organizers

One of the real value-adds of the interactor gem is the concept of Organizers. An Organizer is a special type of interactor that exists only to join other interactors together in sequence (which could themselves be other organizers). If any of the interactors in the list fail, then execution is halted and returns early. This is great for the one-happy-path-with-many-possible-error-states type of business logic. Consider, for example, processing a user signup form:

class ProcessSignup
  include Interactor::Organizer
 
  organize ValidateEmail,
           ValidatePassword,
           CreateUser,
           SendWelcomeEmail
end

In this example, we would run through each step only if the one before it succeeded. The code that called ProcessSignup then only has to handle a single success? or failure? outcome without worrying about all the various ways this could have failed (though the context should probably contain an error message to display to the user or otherwise indicate the exact error that occurred).

The context is also automatically passed from one interactor to the next. So any field added to the context by ValidateEmail will be available to ValidatePassword and so on.

Recommended Conventions

The context can become a bit of global-variable-soup if not cared for properly. To avoid that here's some conventions/rules I would suggest:

1. Use delegate for all incoming arguments used in the interactor

Interactors can use delegate to avoid having to use the context. prefix everywhere. More importantly, they also give you a single place to look to see what arguments are required when calling the interactor.

class ProcessSignup
  include Interactor

  delegate :name, :email, to: :context

 def call
  ...

2. Push return values to the bottom of call

Similar to the first recommendation, by placing outgoing variables at the end of the method you have a single place to look to see what the interactor is returning (if anything). Of course, this is not always possible when you have early returns for errors.

3. Use a common way to report errors

When returning failure via context.fail!(...) you can pass in values that are added to the context. For example: context.fail!(error: "Email is not unique"). The key here is to always use the same convention (e.g. either error: ... or message: ..., not a mix of both). Also, decide whether or not the errors being returned are displayed to users or not (ideally the error is either always intended to be user-readable or never needs to be user readable).

4. Naming conventions

Beyond that, the organizers are where the interactor gem comes into its own. The best benefit is when the interactors are small, self-contained operations that can be re-used by multiple organizers. I'd also recommend you name the interactors after the action they perform.

I'm a self-confessed fan of naming conventions for things like this, for example in GitArborist interactors that handle incoming events all have a Handle prefix, while ones that process user comments have a Process prefix.

Shortcomings

The main thing I wish the gem did differently is handling incoming arguments. While delegate is useful and does its job, I would appreciate a way to specify optional vs required arguments, with the latter raising an exception if it is missing.