Four Uses for ActiveInteraction
Dylan Lederle-Ensign, Former Senior Developer
Article Categories:
Posted on
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:
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
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!