We need to talk about interactors

I’ve noticed a bit of a trend within the Ruby community in recent years that worries me: every other codebase is now attempting to use interactors. That’s not inherently bad, but as we’ll discuss in this article, it has some unforeseen consequences. Luckily I haven’t seen much of this bleed into libraries but, on application code, it has been surprisingly common. Interestingly enough the first time I saw it my reaction was not negative at all: it looked like a very clean way to organize procedures (or commands) keeping things relatively interchangeable.

After months of extensive usage and delving deeper into the codebases, I have noticed several inherent flaws within the usage of this pattern that cannot be overlooked. Moreover, I have observed the formation of bad habits among developers who rely on this pattern. In this article we’re going to be diving into the issues that stem from it and why you should be careful when using it.

What are interactors anyways?

Most interactor gems say they are an implementation of the command pattern and a way to encapsulate what your application does. As wikipedia tells us, it’s a design pattern to encapsulate how to perform an action.

But the real origin is more likely from Uncle Bob’s Clean Architecture, where he calls them Use Case Interactors. These are classes that encapsulate a use-case with pure data structures. These classes are responsible for taking these data structures, performing the use-case and returning another data structure. There’s a pretty good lecture available online where he goes over some of it.

In Ruby they have emerged as an alternative to Service Objects, which took over the Rails world for a while (app/services anyone?), inspired by Services from Domain Driven Design. These classes encapsulate business logic that does not belong in typical models. They are somewhat similar but not quite the same.

To summarize: an interactor encapsulates a single use-case from your application with pure data structures decoupling that logic from the delivery mechanisms. Whether you use a CLI or an HTTP API to create a user, it doesn’t matter for the use-case.

We could represent an user registration use-case with an interactor like this:

class UserRegistration
  def initialize(name:, email:)
    # ...

  def call
    User.create(name: @name, email: @email)

Then controllers, rake tasks, CLIs or any other delivery mechanisms can use this interactor to register a user. The responsibility of the delivery mechanism is to translate their inputs into the data structures used by the interactors and then translate the results back. As an example, a controller could use this.

class UsersController
  def create
    user = UserRegistration.call(user_params)
    head(user.valid? ? :ok : :bad_request)

The interactor pattern is a great abstraction that makes it easy to decouple business logic from other concerns, promoting the consistent re-use of your use-cases.

Where interactors go wrong

Given the interactor pattern is a useful one even if you’re not trying to strictly follow the Clean Architecture philosophy, what exactly is the problem here? We’re now going to dive into the concrete problems of using interactors without proper care.

A specific interpretation of interactors

We have some examples of gems implementing this pattern. To cite a few we have: interactor, active interactor and active interaction.

Aside from adding confusion with the renaming of the concept to interaction, one of the advantages often cited by these gems is the fact that they use an unified interface: the mighty call method. No more guessing if your AuthenticationService responds to call, perform or run!

Another feature that’s often included into it is the concept of a result. Whenever you call call (ha) you receive a result back telling you if the operation was successful or not. This is pretty handy when chaining things together and for the typical boilerplate that props up in Rails controllers.

Here’s a simple interactor using the interactor gem and the accompanying controller:

class CreatePost
  include Interactor

  def call
    post = Post.new(context.attributes)
    post.save ? post : context.fail!(error: "Could not create post")

class PostsController
  def create
    result = CreatePost.call(post_params)
    if result.success?
      render :show
      flash[:error] = result.error
      redirect_back_or_to posts_path

This example already shows basic error handling with results. Some of them include ActiveSupport::Errors somewhere (typically in the result or the interaction itself), allowing you to return user-readable errors directly to your views. These libraries also assume interactors have a single interface and have some form of telling the caller if the result was successful or not.

Here we have the first issue: nowhere in the Clean Architecture’s definition of interactors it’s stated that they must share a single interface. In fact, each use-case may have completely different interfaces. Some might have results, some might not. Some might return objects, others simple primitives. They accept and return different data structures suited for each use-case.

Unfortunately for (what I imagine was) convenience or ease of use, implementations settled on using this single interface for all interactors. Given this is the first real contact developers have with this pattern, it’s already established as a rule even though it’s a library specific decision.

Above all the abstraction of interactors, as the name states if we used the full name, is to wrap a use-case. In other words, this should wrap the outer shell of your application. Interactors should not be used for the implementation details of said use-cases. They should use application-independent entities and other classes with application business logic to perform whatever the use-case states and return a result, if there’s a need for one.

Based on the current implementations, we’re led down a path that narrows the original interactors idea in a very specific way. Perhaps if we kept the original use-case interactors name, most of the issues I’m going to expose here wouldn’t exist, but here we are.

Everything is an interactor

Even though they were meant to represent use-cases, I’ve found that invariably they start to be used for internal details of other interactors.

After all, they have a simple enough interface which leads to focused classes that are more likely to follow the Single Responsibility Principle. The simple interface also makes them easy to be called in chains. Some libraries even provide classes to facilitate that.

But the biggest reason of all is the “hack” that they enable for one of the most commonly known problems of development: naming. Naming interactors is extremely easy compared to naming abstractions. They represent actions, which means they become verbs. Naming everything as a verb almost completely removes the need to think about any abstractions. Let’s take a look at an example.

Say we want to register a user, send him a welcome email then assign him a role. If we write everything as interactors. We have ValidateUser followed by CreateUser, SendWelcomeEmail and finally AssignRoleToUser. All of these wrapped around a bigger interactor called RegisterUser which calls these interactors in sequence.

class RegisterUser
  def call(user_params:)
    user = User.new(user_params)
    ValidateUser.call(user: user)
    CreateUser.call(user: user)
    SendWelcomeEmail.call(user: user)
    AssignRoleToUser.call(user: user)

It’s easy to see how it makes things easier. Since the interface is extremely generic, naming is no longer a concern, our classes are small and have a single responsibility, this code looks clean.

This is similar to how we would implement this in a more functional style. The RegisterUser works like a higher order function composing other functions forwarding the initial parameters and the results of each function to the next one. So now instead of having interactors being used as a single wrapper on a use-case, they are used as wrappers for everything. A single interactor then calls N other interactors, that in turn call M other interactors and so on.

At this point I’d argue a conscious decision should have been made. Are you going to use a functional style of coding for all your business logic or do you want to use an object oriented approach?

This decision is, more often than not, made without intention. Which in turn leads the codebase down a path that requires a very different style of programming. If this is intentional, you will have to deal with the issues mentioned here using functional patterns knowing the trade-offs you are making. If that’s the case then congratulations! It’s a totally valid alternative and you’ve actively chosen to use it. However if this was not an intentional decision (and in my experience this is the most common scenario), it will inevitably lead to all of the issues we’ll discuss here.

Once this pattern of turning everything into an interactor takes hold, it will force you to mix and match OO and functional concepts every step of the way. It affects error handling, how you deal with duplication, abstractions, naming and more. While I’m not saying they can’t be mixed, it should be done with care and consideration.

One abstraction to rule them all

Interactors need some way of passing arguments to them. This varies depending on the implementation but generally speaking they receive a big hash as an argument. Some libraries create DSLs to describe that contract while others leave that as a Hash. Regardless of the implementation details, this defines how you call these classes.

Ruby’s hashes are extremely good and easy to use, which in turn leads them to be overused. It’s like a higher form of primitive obsession. Although hashes are not strictly primitives, for a dynamic language like Ruby, they might as well be.

The problem here is not limited to interactors but it’s definitely amplified by it. So let’s look at a few examples of how this hurts our code. Let’s use the active interaction gem this time, and look at our user registration again. I’m also going to make it a bit more complex to make the issue more apparent.

class RegisterUser < ActiveInteraction::Base
  string :name
  string :email
  string :ip
  hash :identity_provider, default: nil
  record :organization, default: nil
  boolean :send_welcome_email, default: false

  def execute
    if ip_blocked?
      errors.add(:ip, "IP is blocked") 

    user = User.new(name: name, email: email)
    if user.save
      user.create_identity(identity_provider) if identity_provider
      organization.users << user if organization
      send_email if send_welcome_email

In this scenario we have some logic to fail the registration if the IP is blocked, create an identity based on the given identity provider, an organization in which we have to add the user to and a flag to determine if we should send a welcome email afterwards. This is a common scenario on a web-application.

There are some issues here but I’m gonna focus on the primitive obsession: we actually have multiple distinct concerns that are hidden inside of the arguments. Some of the arguments relate to the user, some to the origin of the registration and others to allow the caller to customize the behavior. However these are effectively hidden because we are using only primitives as inputs.

When using these interactor gems it’s common to use only raw primitives or ActiveRecord objects as inputs. It encourages primitive obsession because hashes can describe pretty much anything and so the input of interactors is rarely the POROs that should be used instead. Additionally, since we are using a DSL to describe this, we don’t have the usual parameter list issue that would be caught by a default RuboCop check.

The Clean Architecture interactors state that we have data structures as inputs and outputs of interactors but it does not necessarily mean they need to be primitives. But implementing these as hashes makes it easy to add as many parameters as you’d like without thinking about any other abstractions required.

If we were to separate the concerns appropriately, the IP blocking should not even be inside of the interactor since it’s a delivery method specific concern. You won’t care about IPs if you call this interaction from the CLI.

For the user related arguments like name, email, identity, organization, we have an user input object that’s missing. If we have a simpler use-case where you only need a name and email, following the YAGNI principle we could avoid the abstraction. But as you add more concerns attached to the user we’re registering, an object should encapsulate all that.

UserInput = Struct.new(:name, :email)

Now, I’m pretty sure most of you will already be analyzing this example and rubbing their hands getting ready to point out flaws or alternative implementations that would be much better. But that’s not the point here: my point is about incentives.

This pattern will repeat itself for every single interactor. They will be copied, pasted and altered for new requirements because creating a new interactor is extremely easy, while thinking about the objects involved and abstractions is not.

New requirements can usually be described as new verbs or small changes to the existing ones. The interactor abstraction fits the bill for pretty much any concept. It’s a class that does something and returns something, after all! Almost anything can be framed as an action.

Incentives matter

As I alluded to before, the issue here is with the incentives. Once you have interactors you will want to add more interactors for everything as it will be the default choice. We shift from thinking about objects and how they interact into a rote repetition of adding new commands and/or making them more configurable. Instead of having objects that represent the input, the operation, the user, the notification system, the validator and all else, we now think in terms of procedures/commands.

It is especially bad for juniors and new hires. Most of the time developers look at what has been done before and replicate it. You don’t go against the grain as the default option. If commands are used everywhere, developers are likely to follow suit and maintain the pattern.

We might even know that using an interactor is not the best choice for the given problem. But given that’s how it’s done everywhere else, even if you do bite the bullet and come up with a more OO approach, we need to sell that to the team. It’s a time consuming effort while the incentive is to simply cave in and repeat the pattern.

Roles and interfaces

Interface definition is really important when creating our abstractions. The interactor interface is very open and it obviously does not cover every use case, so you cannot effectively represent anything complex with it.

We can’t, for example, have a subscriber abstraction with interactors. If we define that interface as responding to #subscribe(object) and #unsubscribe(object) messages, the narrow definition of interactors can’t handle that. That’s somewhat expected since a subscriber is not really a command or a use-case.

Regardless, with interactors as the standard, you would certainly find SubscribeToSomething interactors, alongside another UnsubscribeFromSomething for unsubscribing. Once again, this is very much a functional approach.

## Interactors
class Subscribe
  def call(object, subscriber)
    object.subscribers << subscriber

class Unsubscribe
  def call(object, subscriber)
    object.subscribers.delete_if { _1 == subscriber }

## OO

class Thing
  def subscribe(subscriber)
    subscribers << subscriber

  def unsubscribe(subscriber)
    subscribers.delete_if { _1 == subscriber }

# Or with a module

module EventEmitter
  def subscribe(subscriber)
    subscribers << subscriber

  def unsubscribe(subscriber)
    subscribers.delete_if { _1 == subscriber }

  def subscribers
    @subscribers ||= []

class Thing
  include EventEmitter

Interactors don’t really have any state and act like mere functions. When using them we no longer think in terms of roles like a subscriber, event emitter, etc. We only think about actions. We lose interfaces and don’t gain any of the advantages that functional programming gives us, since we don’t adopt other concepts like: immutability, currying, higher-order functions and so many others.

With the original use-case interactors and an object oriented approach, we would use objects - including ones with complex interfaces - to accomplish what we need. If we want to turn everything into a function, then we must also use everything else that comes with functional programming too or we end up with a poor man’s functional nightmare.

The roles and interfaces that are so important for OO are completely lost with this approach. Many developers already struggle with basic modeling so this is disastrous. There’s a precedent in place to almost completely circumvent thinking about your models and domain. Simply add another action.


Let’s take a look at another real world example related to interactors I’ve come across. The operation in question published a specific resource to different online platforms. My job was to refactor that code and it was pretty clear from the get-go what had happened - later confirmed by colleagues.

I’m sure many of you will have had the same experience: the system is built to integrate with one platform, then we want to add support for a new one. The code was similar but not quite the same and, since interactors were the standard, the usual was done: a new interactor was added. Later, when a third platform needed to be integrated, we ended up with three interactors and a switch case depending on the platform type.

class PublishThing
  def call(thing)
    case platform
    when FirstPlatform
    when SecondPlatform
    when ThirdPlatform

Upon analyzing this code, it became apparent that there were methods that were re-created multiple times with slight differences, entire workflows that were also re-created and, in reality, the overall procedure did not change significantly for each platform. The aftermath was a ton of duplication, bugs and slight differences between each implementation that made the code brittle and hard to maintain.

class Platform
  # platform specific way of publishing
  def publish(thing); end

  # platform specific way of doing something after publishing
  def after_publish(thing); end

  # other platform shared attributes/methods

# platform specific implementations
class FirstPlatform < Platform; end
class SecondPlatform < Platform; end
class ThirdPlatform < Platform; end

class PublishThing
  def call(thing, platform)
    # Shared publishing logic

It was very clear to me how interactors shaped that code. Instead of having a single operation with different classes encapsulating the differences, we have three distinct operations that all respond to #call. Creating a new interactor and implementing everything there is easier than thinking about the abstractions needed to make the entire system following the open/closed principle. The reality is we only have one use-case: publish the thing.

While there is a threshold where duplication is accepted and even encouraged, once you exceed that point, reasoning about the system and its abstractions becomes crucial for a better codebase. But interactors steer you into the duplication path. Be it intentional or unintentional it ends up littering our code more often than not.

Most times duplication isn’t obvious at first and only apparent after multiple instances of the behavior show up. The right time to refactor something is not set in stone and creating abstractions early can be even worse depending on how much you know about the problem space.

Developers, much like all humans, take the easier route and add to that case statement without even considering potential duplications. This is, of course, not strictly an issue with interactors, but it’s most certainly accentuated by them. The default is to always add a new class that “does the deed” and add to the conditional.

In this particular case we had to understand where the platforms diverged and keep in mind that there is only one use-case. But since most of that code was copied over and edited, it was not obvious. Incentives that are amplified by interactors led to unintended duplication.

Results and error handling

Another area that is usually affected by this pattern is error handling. It’s very common to have to distinguish between different failure types. For example, we may need to abort execution on a specific error and retry on another.

With interactors we then have to make some non-optimal choices. We can fail the interactor and add errors to it. But this then means the caller has to rely on strings or error types:

# on the interactor
errors.add(:base, :my_custom_error) unless valid?

And then our caller has to check the result with something like:

result = Interactor.call
if result.failed?
  if result.errors.added?(:base, :my_custom_error)
    # handle custom error
  elsif result.errors.added?(:base, :another_custom_error)
    # handle another custom error

Not ideal to say the least. We’re back to checking strings/symbols to control our flow. Another option is to use the return value for this:

result = Interactor.call
if result.failed?
  if result.error_type == :my_custom_error
    # handle custom error
  elsif result.error_type == :another_custom_error
    # handle another custom error

Not ideal either. You have to add this value to the return object to identify the type of error that occurred.

Handling errors beyond basic boolean results forces you to use unconventional techniques to achieve a functionality that our language provides naturally: exceptions.

An exception allows you to create a specific type of error and stop the execution. It also includes contextual information about the error and you can customize it to your heart’s content. After all, it’s simply a PORO.

I know what you’re thinking: you should not use exceptions for control flow! I agree that there are cases where exceptions are not a great fit. The most typical example is input validation. But aside from that, almost every error should stop the execution of the program and be dealt with accordingly. An alternative can be found on languages like Go where pretty much every function call looks like this:

ok, err := doSomething()
if err != nil {
  // deal with it

We can debate the trade-offs from these approaches but that’s a moot point: we are in Ruby, we have exceptions and diverging from it should not be done by default.

Using exceptions with interactors is not prohibited and we can have both use-cases. But interactors change the default behavior. As the caller you are now required to check the results as if every operation has a failure mode. Even if you use bang versions of call, which most gems give you, we’re now artificially creating an exception with a generic type that we cannot easily distinguish.

# using call!
rescue InteractorFailure
  # What actually failed here?

# traditional exceptions
rescue SpecificError
rescue AnotherSpecificError
rescue GenericError

As another example of the consequences of this change, it becomes very common to encounter interactors called ListSomething in which there is no failure mode. You either get a list or we should have an exception. But we are then forced to check results or use the generic bang version of call obscuring the original exception.

If we interpret interactors as an implementation of the command pattern, then this is not a command, it’s a query! What do we gain from wrapping that?

Ruby isn’t like Go and we don’t have a compiler to tell us when we forgot to check for an error like Elm or Haskell. As a result many times interactors are called without the bang or explicit result checking, causing errors to go unchecked. This in turn results in exceptions down the call chain (usually whiny nil errors). It’s surprisingly common to see interactors being called without error checking and having errors being silently swallowed.

One could argue that’s an issue with dynamic languages (although most compiled languages don’t check for logical errors) but once again this is a moot point: we are in Ruby and that’s not the language’s default.

I cannot tell you how many times I’ve had to investigate exceptions like InteractorException: X is invalid. Upon a closer look, there are dozens of interactors that can raise the same error. And given the possibility of some interactors being called without the bang, masking the real origin of the error is common.

These issues with error handling are amplified for juniors. When do you return a false result and add a string error message? When do you raise an exception? When do you use the bang method? Is returning a falsy result the same as a runtime exception? Should you wrap all exceptions in your interactor to keep the interactor pattern? I’ve seen many senior developers using this incorrectly because interactors incentivize boolean operation results.

The answers to these questions are not clear and they will vary depending on various factors. When using interactors you are faced with this question constantly and I have yet to see a codebase that properly enforces these rules. The idea of returning a boolean result for every operation only adds confusion with little to no benefit when it comes to error handling.

Returning “NaN” for zero divisions as JavaScript does is not how we do it in Ruby. Our standard library will raise a ZeroDivisionError for a reason, even though that is a known failure use-case for a division. For some reason though, when it comes to interactors, we suddenly feel like it’s okay to handle errors with boolean values by default.

Do you really need an interactor?

This all leads to this question: do you really need an interactor? Let’s take a look at another example using the interactor gem:

class SendOnlineNotification
  def call(user:)
    WebSocket.send_notification(user, :online)

On this implementation, we assume the interactor does not have a failure state and any errors that happen will have to be exceptions. So if that’s the case then why is this an interactor at all? It adds no real value to us.

Alternatively we make this even worse:

class SendOnlineNotification
  include Interactor

  def call
    WebSocket.send_notification(context.user, :online)
    context.fail!(message: 'websocket.fail')

With this alternative we are now wrapping exceptions into a failure. As a consequence we are losing all of the context from the exception and replacing a meaningful error, stack trace and message with a cryptic message string (most gems translate the key with i18n). On other interactor gems you’d add websocket.fail to the interactor’s errors attribute but the point remains.

Regardless, it is now the responsibility of the caller to check for this failure while having no meaningful way of differentiating types of errors. We’ve effectively destroyed the entire purpose of exceptions in order to satisfy the boolean result nature of interactors.

There are cases where wrapping exceptions into boolean results make sense but those are, in my experience, the exception (pun intended) and not the rule.

The harsh reality is that we do not need to use the command pattern in the absolute majority of cases. The desire to pigeonhole all business logic into boolean outcomes makes error handling way more difficult than it should be and ends up causing a lot more harm than good.

You sure look like a nail

It does not matter how much we want to believe in theories, reality always hits you in the face. Interactors are very addictive to use. When you compare them to the classic MVC with service objects, it seems like you now have a place to put everything you never knew where to put before. Everything can be an interactor because everything does something! And everything returns a result too! On top of that now we can chain it all together with ease while keeping classes small. Finally, no more fussing with names!

On every codebase with interactors I’ve come across, they spread like wildfire. Once that happens problems like the ones we discussed arise and become common. I’ve seen interactors being used even to instantiate objects: BuildSomeObject.call. I guess why not, right?

Honestly, I’d love to say that this is a case of inexperienced and/or bad developers, but it’s not. No matter how experienced you are, when faced with deadlines or other factors that force us to compromise, we will take the easiest path and duplicate the behavior. That’s might be the right option sometimes, as Sandi Metz put it before, the wrong abstraction is worse than duplication. But once you introduce interactors, it’s almost inevitable that duplication becomes the default. Best case scenario we create those tech debt tickets: the ones we never work on because they’re not critical.

Navigating that maze of interactors with procedures calling procedures that call other sequential procedures becomes the norm. Domain objects are absent or rare, duplication is more common than before and yet we feel safe with every new interactor we add because it’s a small class with a simple interface that’s easy to name.

Interactors are a hammer and everything looks like a nail once you’re using it.

If it’s so bad, why do people use it?

The first obvious reason would be to use the concept from Clean Architecture. The ideas behind it are solid and enticing for developers to implement, but we should always be careful of doing so in a void. This is a case where implementations took an idea and implemented it in a dangerous manner that ends up harming codebases more than helping them.

Another reason is because it somewhat forces developers to make small classes. I’ve lost count of how many times people said they improved their code with interactors because now we have smaller classes. But that’s a perfectly valid reason to create small classes, not for interactors! We trade so many language features, basic OO principles and add all of the drawbacks we’ve mentioned here for something we can achieve without it.

I’ve heard from others that using the single #call interface removes confusion on how to use each class. Well, I guess? But do we really gain anything from making every single object respond to the same generic interface? I’d argue it’s not worth the price.

There are way more reasons I haven’t heard of, so I’d love to learn about them if you have one.

What can we do about it?

If you need the command pattern, I’d suggest you simply write the code yourself. Honestly, creating an interface for commands and a class for results with POROs is extremely simple. If you do it yourself you can also take advantage of amazing Ruby features like types instead of hashes, positional/named arguments, exceptions and much more that is lost on these gems.

More importantly use it when you actually need a command pattern. Perhaps you need the ability to chain, rollback or store commands for later usage. That’s why the pattern was created in the first place so it’s a great tool for the job.

A dislike for the Service Objects pattern is one of the reasons the majority of the developers I’ve talked to turned to interactors in the first place. Services grew too large and developers don’t split those classes as much as they should. They don’t know what to call these concepts and/or where to put them, so interactors seem to solve that: Verb.call to the rescue. It’s so much easier to name classes as verbs and use the same interface everywhere but that comes at a cost.

If you really want to use interactors from Clean Architecture, then create those classes yourself and for the love of all that’s good only use them for use-cases! Define the interfaces for your use-case interactors yourself and create explicit contracts between your application and the delivery mechanisms. They should be the interface to your app and nothing more. Don’t use them for your internals. Rely as much as possible on your entities and POROs.

The main reason I wrote this article was to raise the flag that: interactors are not a silver bullet! It’s easy to forget this after they’ve been introduced to your codebase. Some might say this is obvious and, perhaps it should be. But if it really was, I wouldn’t feel like I had to write this after seeing the same problems happen on completely different codebases.

We don’t need these libraries to create good OO code. We can (and should) create more domain objects and strive to keep classes small at all times. There’s no substitute for good engineering and design. Sadly I can’t say “use this instead” because there is nothing that fits that bill. We’d all love to have an incredible hack that covers it all, but there isn’t one.

For me, coming back to those basic OOP examples like Car -> Engine -> Piston -> Fuel Injector is something that always helps. It’s much harder with the abstract concepts we have to deal with, but it’s not impossible. Objects communicating through meaningful messages and classes representing these concepts are, for me, still the way to go.

Or if you do want to use functional programming, do so with all of its tools. Don’t cherry pick one piece without all of the other concepts and patterns needed for a good FP codebase.

So if there is only one thing you pick up from this article, let it be: if you want to promote good object oriented programming patterns in your codebase, be very careful with interactors.

It invariably comes down to taking the time to think through our solutions. Nothing can substitute that.