Four Uses for ActiveInteraction

ActiveInteraction is a handy Ruby gem that helps keep Rails code clean. This posts describes four ways we used it on a recent project.

On a recent project we made extensive use of the ActiveInteraction gem. Its a Ruby implementation of the command pattern that works well with Rails for validating inputs, performing isolated pieces of work, and cleanly returning results or errors. The project was a JSON API which interacted with a number of external services. We were tasked with getting the project started, establishing some conventions and patterns, and then handing it back to an internal team to finish development. At the end of the engagement, our team was happy with the code and the client team was able to hit the ground running.

ActiveInteraction

ActiveInteraction is “an implementation of the command pattern in Ruby”. This post which announced the gem does a nice job of explaining the motivation behind the gem. It came from a rails project which was running into a fairly common problem: bloated controllers. The developers wanted to leave their models concerned with persistence and keep their controllers concerned with request logic. But where to put the business logic that the application was actually supposed to implement? Enter the interactor pattern, which houses application specific business rules in interactor objects, and isolates details like which database the application uses, or whether it renders the data as html views or json payloads. The interactor is responsible for validating what input the application accepts, performing some work, and returning whether the work was successful or not.

ActiveInteraction takes this concept and implements it for use in a Rails environment. ActiveInteraction objects respond to the same validations and errors API that ActiveModel does, so they can be seamlessly integrated into Rails forms. One of our favorite features was composition, which lets you call an interactor from within another interactor. If the called interactor fails, it will terminate and add its errors to the caller. This lets you nest interactors several layers deep without the top level needing to handle lower level failures or know anything about the interactor that caused it.

Our uses

We found the command pattern really handy, and used it in several different ways: for controller actions, to handle state transitions, to wrap external services, and for use as miscellaneous service objects. I’ll give examples of each, in a fictional domain, and provide some assessment as to whether it ended up being a good use case for the pattern or not.

1) Controller Actions:

Every public controller method was backed by a corresponding interaction defined in app/controllers/controller_actions/MODEL_NAME/ACTION_NAME. A controller action takes inputs from the controller and returns a serialized object.

Our controllers actions looked like:

def create
  inputs = create_params
  outcome = ControllerActions::Animals::Create.run(inputs)
  render_outcome(outcome, success_status: 201)
end

Which was backed by an interaction like:

module ControllerActions
  module Animals
    # Interaction for AnimalsController#create
    class Create < ::ControllerActions::Base
      date_time :birthday, default: nil
      string :name
      string :favorite_food

      def execute
        serialize(animal, with: AnimalSerializer)
      end

      private

      def animal 
        @animal ||= compose(
          ::Animals::Create,
          name: name,
          favorite_food: favorite_food,
          birthday: birthday
        )
      end
    end
  end
end

Evaluation: This is the motivating use case of the gem (moving logic out of the controller) and overall I think it was a slight improvement. Many of the interactors ended up being quite simple, like this example, but others wrapped up more complex conditions in a simple, testable interface. It let us write helpers for the controller like render_outcome:

def render_outcome(outcome, success_status: 200, error_status: 422)
  if outcome.valid?
    render_success(outcome.result, status: success_status)
  else
    render_error(outcome.errors, status: error_status)
  end
end

def render_success(result, status:)
  render status: status,
         json: { data: result }
end

All of this helped to DRY up our code and keep the controllers very tight. One thing you may notice is that this application is only presenting a JSON api. While that was one of the original requirements, I think we should have kept the serialization at the controller level, and left these controller interactors ignorant of that detail.

2) State Transitions: Many of our models used AASM for state machines. Each state transition should be called from within a corresponding state_transition interaction. For example, to transition an animal to sleeping, instead of calling the method defined by AASM (my_animal.sleep!), we invoke StateTransitions::Animals::Sleep.run(animal: my_animal). This interaction performs any preparation that needs to happen before the transition and issues any side effects that it triggers. Those conditions go in their own file, rather than within the model that defines the state machine:

Ruby
module StateTransitions
  module Animals
    class Sleep < ::StateTransitions::BaseInteraction
       object :animal

         def execute
           # Do any guards
           return unless animal.sleepy?

           # Or actions that need to precede the transition
           animal.prepare_bed

           # Do the actual transition
           animal.sleep! 

           # Do any post transition actions
           notify!(“#{animal.name} has gone to bed! Don’t disturb for at least 8 hours please”)
         end
      end
    end
  end
end
Ruby
aasm do
  state :awake, initial: true
  state :sleeping

  event :sleep do
    before do
      prepare_bed
    end
    transitions from: :awake,
                to: :sleeping,
                guard: :sleepy?,
                after: Proc.new { notify!("#{name} has gone to bed! Don’t disturb for at least 8 hours please") }
  end
end

Evaluation: With one state transition, one guard and one callback the suggested AASM style is manageable, but it quickly becomes difficult to reason about. Moving to a straightforward function that can be read from top to bottom and written in regular ruby, rather than a DSL, felt like a big win. Of course, some of these state transition interactors ended up with just the state transition (i.e animal.sleep!) and a log event. Writing those certainly felt like overkill, but it was essential to enable composition of the interactors. Overall, I wouldn’t say this was a clear improvement from the callback style, but it wasn’t worse.

3) External Services: Our application had to talk to several external services and we wrapped all calls to them in interactions defined in app/services. This included Stripe, the existing Rails monolith we were extracting functionality from and an external logger.

Evaluation: This was a clear win. External services (even excellent ones like Stripe) can fail for all kinds of reasons. We handled those failures in this layer, and nobody else had to know about it. It was also very easy to mock them out to keep tests fast. Wrapping external services is not inherent to the command pattern, but it worked well for the task.

4) Miscellaneous: There were several interactions that didn’t fit into these three categories that remained in app/interactions. These are discrete tasks that other applications might call service objects. When we started the project, we followed the ActiveInteraction docs suggestion of putting interactions in app/interactions, grouped by model. From this, we noticed patterns and refactored out the state transitions and controller actions, but there remained some things we couldn’t find a better place for.

Evaluation: This ended up housing some of the more complex functionality of the app, and some of the most important business logic. It also didn’t end up having a clear layer to slot into. These classes used interactors from the other layers and in turn were used by them. This created some coupling that some future refactors could target.

Conclusion

Overall, the command pattern supported our architecture really well. As advertised, it let us keep controllers simple and models concerned with persistence. It validated external data, defining and enforcing clear boundaries for our application. The ActiveInteraction implementation worked great with Rails, particularly composing interactors. Give it a try and let us know how it works for you!

Dylan Lederle-Ensign

Dylan is a developer who leans on rigorous academic training to solve practical problems for real users and organizations. He works in our Falls Church, VA, office.

More articles by Dylan