Skip to content
Composable Ruby service objects
Ruby Shell
Branch: master
Clone or download

Latest commit

Latest commit 00ed711 Mar 24, 2020


Type Name Latest commit message Commit time
Failed to load latest commit information.
.github/workflows Add badge for tests Mar 15, 2020
bin Create gemspec Mar 14, 2020
spec Extra specs Mar 24, 2020
.gitignore Create gemspec Mar 14, 2020
.rspec Extra specs Mar 24, 2020
.ruby-version Ruby version Mar 12, 2020 v2.0.0 Mar 24, 2020
Gemfile Create gemspec Mar 14, 2020
Gemfile.lock v2.0.0 Mar 24, 2020
LICENSE.txt Minor README udpates Mar 24, 2020
Rakefile Add RuboCop to rake task Mar 15, 2020
service_actor.gemspec Extra specs Mar 24, 2020



This Ruby gem lets you move your application logic into into small composable service objects. It is a lightweight framework that helps you keep your models and controllers thin.



Add these lines to your application's Gemfile:

# Composable service objects
gem 'service_actor'


Actors are single-purpose actions in your application that represent your business logic. They start with a verb, inherit from Actor and implement a call method.

# app/actors/send_notification.rb
class SendNotification < Actor
  def call

Trigger them in your application with .call: # => <ServiceActor::Result…>

When called, actors return a Result. Reading and writing to this result allows actors to accept and return multiple arguments. Let's find out how to do that.


To accept arguments, use input to create a method named after this input:

class GreetUser < Actor
  input :user

  def call
    puts "Hello #{}!"

You can now call your actor by providing the correct arguments: User.first)


An actor can return multiple arguments. Declare them using output, which adds a setter method to let you modify the result from your actor:

class BuildGreeting < Actor
  output :greeting

  def call
    self.greeting = 'Have a wonderful day!'

The result you get from calling an actor will include the outputs you set:

result =
result.greeting # => "Have a wonderful day!"


Inputs can be marked as optional by providing a default:

class BuildGreeting < Actor
  input :name
  input :adjective, default: 'wonderful'
  input :length_of_time, default: -> { ['day', 'week', 'month'].sample }

  output :greeting

  def call
    self.greeting = "Have a #{adjective} #{length_of_time} #{name}!"

This lets you call the actor without specifying those keys:

result = 'Jim')
result.greeting # => "Have a wonderful week Jim!"

If an input does not have a default, it will raise a error:

result =
=> ServiceActor::ArgumentError: Input name on BuildGreeting is missing.


You can add simple conditions that the inputs must verify, with the name of your choice under must:

class UpdateAdminUser < Actor
  input :user,
        must: {
          be_an_admin: ->(user) { user.admin? }


In case the input does not match, it will raise an argument error.

Allow nil

By default inputs accept nil values. To raise an error instead:

class UpdateUser < Actor
  input :user, allow_nil: false



Sometimes it can help to have a quick way of making sure we didn't mess up our inputs.

For that you can use the type option and giving a class or an array of possible classes. If the input or output doesn't match is not an instance of these types, an error is raised.

class UpdateUser < Actor
  input :user, type: User
  input :age, type: [Integer, Float]


You may also use strings instead of constants, such as type: 'User'.

When using a type condition, allow_nil defaults to false.


All actors return a successful result by default. To stop the execution and mark an actor as having failed, use fail!:

class UpdateUser
  input :user
  input :attributes

  def call
    user.attributes = attributes

    fail!(error: 'Invalid user') unless user.valid?


This will raise an error in your app with the given data added to the result.

To test for the success of your actor instead of raising an exception, use .result instead of .call. This lets you use success? and failure? on the result.

For example in a Rails controller:

# app/controllers/users_controller.rb
class UsersController < ApplicationController
  def create
    result = UpdateUser.result(user: user, attributes: user_attributes)
    if result.success?
      redirect_to result.user
      render :new, notice: result.error

Any keys you add to fail! will be added to the result, for example you could do: fail!(error_type: "validation", error_code: "uv52", …).

Play actors in a sequence

To help you create actors that are small, single-responsibility actions, an actor can use play to call other actors:

class PlaceOrder < Actor
  play CreateOrder,

This creates a call method that will call every actor along the way:…)

The first actor will receive the inputs and pass the result along to the next actor. In fact, every actor along the way shares the same set of results until it it finally returned.


When using play, when an actor calls fail!, the following actors will not be called.

Instead, all the actors that succeeded will have their rollback method called in reverse order. This allows actors a chance to cleanup, for example:

class CreateOrder < Actor
  output :order

  def call
    self.order = Order.create!(…)

  def rollback

Rollback is only called on the previous actors in play and is not called on the failing actor itself. Actors should be kept to a single purpose and not have anything to clean up if they call fail!.


You can use inline actions using lambdas. Inside these lambdas, you don't have getters and setters but have access to the shared result:

class Pay < Actor
  play ->(result) { result.payment_provider = "stripe" },
       ->(result) { result.user_to_notify = result.payment.user },

Like in this example, lambdas can be useful for small work or preparing the result for the next actors. If you want to do more work before, or after the whole play, you can also override the call method. For example:

class Pay < Actor

  def call
    Time.with_timezone('Paris') do

Play conditions

Actors in a play can be called conditionally:

class PlaceOrder < Actor
  play CreateOrder,
  play NotifyAdmins, if: ->(result) { result.order.amount > 42 }

You can use this to trigger an early success.

Build your own actor

If you application already uses an "Actor" class, you can build your own by changing the gem's require statement:

gem 'service_actor', require: 'service_actor/base'

And building your own class to inherit from:

class ApplicationActor
  include ServiceActor::Base


In your application, add automated testing to your actors as you would do to any other part of your applications.

You will find that cutting your business logic into single purpose actors makes your application much simpler to test.


This gem is heavily influenced by Interactor ♥. However there are a few key differences which make actor unique:

  • Does not hide errors when an actor fails inside another actor.
  • Requires you to document all arguments with input and output.
  • Defaults to raising errors on failures: actor uses call and result instead of call! and call. This way, the default is to raise an error and failures are not hidden away because you forgot to use !.
  • Allows defaults, type checking, requirements and conditions on inputs.
  • Delegates methods on the context: foo vs, = vs = , fail!!`.
  • Shorter setup syntax: inherit from < Actor vs having to include Interactor and include Interactor::Organizer.
  • Organizers allow lambdas, being called multiple times, and having conditions.
  • Allows early success with conditions inside organizers.
  • No before, after and around hooks, prefer using play with lambdas or overriding call.

Thank you to @nicoolas25, @AnneSottise & @williampollet for the early thoughts and feedback on this gem.


After checking out the repo, run bin/setup to install dependencies. Then, run rake to run the tests and linting. You can also run bin/console for an interactive prompt.

To release a new version, update the version number in version.rb, and in the, run rake, and create a commit for this version. You can then run rake release, which will create a git tag for the version, push git commits and tags, and push the gem to


Bug reports and pull requests are welcome on GitHub.

This project is intended to be a safe, welcoming space for collaboration, and everyone interacting in the project’s codebase and issue tracker is expected to adhere to the Contributor Covenant code of conduct.


The gem is available as open source under the terms of the MIT License.

You can’t perform that action at this time.