Skywalker is a gem that provides a simple command pattern for applications that use transactions. (Or not! In later versions, Skywalker is much more modular and can be used for non-transactional purposes, too.)

use transactions.

Why Skywalker?

It's a reference to 'Commander Skywalker' from Star Wars, a rank the main protagonist achieves and by which he is called.

It's tricky to come up with a memorable single-word name for a gem about commands that's at least marginally witty. If you can't achieve wit or cleverness, at least achieve topicality, right?

What is a command?

A command is simply a series of instructions that should be run in sequence and considered a single unit. If one instruction fails, they should all fail.

That's a transaction, you say? You're correct! But there are some benefits of considering transactional blocks as objects:

Operations as Objects (And Interfaces as Collaborators)

One of the really cool things about Ruby is that it's trivial to pass a class as an argument. This makes Dependency Injection (DI) dead simple. (If you've never heard of DI, you should read my favourite architecture grump Piotr Solnic's take on it.)

This, of course, makes a lot of sense when you'd like to remove a reference to a model class to test in isolation. But it also makes a lot of sense when you realize that portions of your code are collaborators, too: anything that's orthogonal (or a side effect) to what you're working on is something that you can test in isolation. And if you've ever done this, you know that isolated tests are so much easier to write, so much easier to maintain, and so much faster to run.

This isn't limited to a Command object. You can offload these chunks of code into numerous other collaborators, some of which people call 'Service', 'Operation', or 'Policy' objects. But it is an essential property of these types of objects and a huge benefit.

Skywalker also has an Acceptable module that can be used to create other such objects that do not necessarily require a transaction.


Skywalker places a strong emphasis on dependency injection.

This means that you can unit test the command for correctness without having to do a full integration test for every single path through the code. That makes your test suite lean and mean, and encourages you to aim for weaker forms of coupling (i.e. preferring connascence of name, rather than identity).

Best practice is to describe the operations in methods, which can then be stubbed out to test small portions in isolation.

This also allows you to make the reasonable inference that the command will abort properly if one step raises an error, and by convention, the same method (on_failure) will be called. In most cases, you can thereby verify happy path and a single bad path through integration specs, and that will suffice.


The benefit of abstraction means that you can easily reason about a command without having to know its internals. Standard caveats apply, but if you have a CreateGroup command, you should be able to infer that calling the command with the correct arguments will produce the expected result.

Knowledge of Results Without Knowledge of Response

A command prescriptively takes callbacks or #callable objects, which can be called depending on the result of the command. By default, Skywalker::Command can handle an on_success and an on_failure callback, which are called after their respective results. You can define these in your controllers, which lets you run the same command but respond in unique ways, and keeps controller concerns inside the controller.

You can also easily override which callbacks are run. Need to run a different callback if request.xhr?? Simply override run_success_callbacks and run_failure_callbacks and call your own.

A Gateway to Harder Architectures

It's not hard to create an Event class and step up toward full event sourcing, or to go a bit further and implement full CQRS. This is the architectural pattern your parents warned you about.


Add this line to your application's Gemfile:

gem 'skywalker'

And then execute:

$ bundle

Or install it yourself as:

$ gem install skywalker


Let's talk about a situation where you're creating a group and sending an email inside a Rails app.

Standard operating procedure usually falls into one of two patterns, both of which are mediocre. The first makes use of ActiveRecord callbacks:

# app/controllers/groups_controller.rb
class GroupsController < ApplicationController
  # ...

  def create
    @group =

      redirect_to @group, notice: "Created the group!"
      flash[:alert] = "Oh no, something went wrong!"
      render :new

# app/models/group.rb
class Group < ActiveRecord::Base
  after_create :send_notification


  def send_notification

This might seem concise because it keeps the controller small. (Fat model, thin controller has been a plank of Rails development for a while, but it's slowly going away, thank heavens). But there are two problems here: first, it introduces a point of coupling between the model and the mailer, which not only makes testing slower, it means that these two objects are now entwined. Create a group through the Rails console? You're sending an email with no way to skip that. Secondly, it reduces the reasonability of the code. When you look at the GroupsController, you can't immediately see that this sends an email.

Moral #1: Orthogonal concerns should not be put into ActiveRecord callbacks.

The alternative is to keep this inside the controller:

# app/controllers/groups_controller.rb
class GroupsController < ApplicationController
  # ...

  def create
    @group =

      redirect_to @group, notice: "Created the group!"
      flash[:alert] = "Oh no, something went wrong!"
      render :new

This is more reasonable, but it's longer in the controller and at some point your eyes begin to glaze over. Imagine as these orthogonal concerns grow longer and longer. Maybe you're sending a tweet about the group, scheduling a background job to update some thumbnails, or hitting a webhook URL. You're losing the reasonability of the code because of the detail.

Moreover, imagine that the group email being sent contains critical instructions on how to proceed. What if NotificationMailer has a syntax error? The group is created, but the mail won't be sent. Now the user hasn't received a good error, and your database is potentially fouled up by half-performed requests. You can run this in a transaction, but that does not reduce the complexity contained within the controller.

Moral #2: Rails controllers should dispatch to application logic, and receive instructions on how to respond.

The purpose of the command is to group orthogonal but interdependent results into logical operations. Here's how that looks with a Skywalker::Command:

# app/controllers/groups_controller.rb
class GroupsController < ApplicationController
  # ...

  def create
      on_success: method(:on_create_success),
      on_failure: method(:on_create_failure)

  def on_create_success(command)
    redirect_to, notice: "Created the group!"

  def on_create_failure(command)
    flash[:alert] = "Oh no, something went wrong!"
    @group =
    render :new

# app/commands/create_group_command.rb
class CreateGroupCommand < Skywalker::Command
  def execute!

  private def required_args
    %w(group on_success on_failure)

  private def save_group!!

  private def send_notifications!

  private def notifier
    @notifier ||= NotificationsMailer.group_created_notification(group)

Two notes on the above example:

First, you do not have to use method. You are free to do what you wish—whether that is using a proc, or injecting the controller context as an argument and then overwriting the callback methods to use that. But I (and this library) take a principled stance that what occurs inside those callbacks is usually the responsibility of the controller and should remain within it, so this is made easy, and you are strongly encouraged to follow this pattern.

Secondly, it is ideologically 'purer' to pass in a method which would construct a group, and the params separately, because it moves the instantiation of domain concepts out of the controller. However, for the purpose of 'GSD', I often wind up keeping the instantiation of simple AR objects inside of my controller.

Basic Composition Summary

Compose your commands:

require 'skywalker/command'

class AddGroupCommand < Skywalker::Command
  def execute!
    # Your transactional operations go here. No need to open a transaction.
    # Simply make sure each method raises an error when it fails.

Then call your commands:

command =
  any_keyword_argument: "Is taken and has an attr_accessor defined for it."

You can pass any object responding to #call to the on_success and on_failure handlers, including procs, lambdas, controller methods, or other commands themselves.

What happens when callbacks fail?

Exceptions thrown inside the success callbacks (on_success or your own callbacks defined in run_success_callbacks) will cause the command to fail and run the failure callbacks.

Exceptions thrown inside the failure callbacks (on_failure or your own callbacks defined in run_failure_callbacks) will not be caught and will bubble out of the command.

Overriding Methods

The following methods are overridable for easy customization:

  • execute!
    • Define your operations here.
  • required_args
    • An array of expected keys given to the command. Raises ArgumentError if keys are missing.
  • validate_arguments!
    • Checks required args are present, but can be customized. All instance variables are set by this point.
  • transaction(&block)
    • Uses an ActiveRecord::Base.transaction by default, but can be customized. execute! runs inside of this.
  • confirm_success
    • Fires off callbacks on command success (i.e. non-error).
  • run_success_callbacks
    • Dictates which success callbacks are run. Defaults to on_success if defined.
  • confirm_failure
    • Fires off callbacks on command failure (i.e. erroneous state), and sets the exception as command.error.
  • run_failure_callbacks
    • Dictates which failure callbacks are run. Defaults to on_failure if defined.

For further reference, simply see the command file. It's less than 90 LOC and well-commented.

Testing (and TDD)

Take a look at the examples directory, which uses the example as above of a notifier, but makes it a bit more complicated: it assumes that we only send emails if the user (which we'll pass in) has a preference set to receive email.


Here's what you can assume in your tests:

  1. Arguments that are present in the list of required_args will throw an error before the command executes if they are not passed.
  2. Operations that throw an error will abort the command and trigger its failure state.
  3. Calling is functionally equivalent to calling


There are two tests that you need to write. First, you'll want to write a Command spec, which are very simplistic specs and should be used to verify the validity of the command in isolation from the rest of the system. (This is what the example shows.) You'll also want to write some high-level integration tests to make sure that the command is implemented correctly inside your controller, and has the expected system-wide results. You shouldn't need to write integration specs to test every path -- it should suffice to test a successful path and a failing path, though your situation may vary depending on the detail of error handling you perform.

Here's one huge benefit: with a few small steps, you won't need to include rails_helper to boot up the entire environment. That means blazingly fast tests. All you need to do is stub transaction on your command, like so:

RSpec.describe CreateGroupCommand do
  describe "operations" do
    let(:command) { double("group") }

    before do
      allow(command).to receive(:transaction).and_yield

    # ...

Common Failures

Before version 3.0, you might have received an error like this:

undefined method `call' for nil:NilClass
  # .../lib/skywalker/command.rb:118:in `run_failure_callbacks'

This means that the command failed and you didn't specify an on_failure callback. You can stick a debugger inside of run_failure_callbacks, and get the failure exception as self.error. You can also reraise the exception to achieve a better result summary, but this is not done by default, as you may also want to test error handling.

Now, in version 3.0 and later, any command that does not receive a callable on_failure argument will raise any arguments it encounters during the process of running execute!.


A Skywalker::Command is implemented through a series of modules that can be used independently from each other and outside of the context of the Command object.


Skywalker::Acceptable allows an object to receive a keyword list of arguments upon instantiation. It creates a reader and writer for each keyword that doesn't already have one, and it will raise an error for any keyword not given that is present inside its required_args list.


require 'skywalker/acceptable'

class MyClass
  include Skywalker::Acceptable

  def required_args

  def bar
    "definitely not #{@bar}"

  def baz=(int)
    @baz = int.to_s.reverse.to_i
end "abc", bar: "xyz") # => ArgumentError, "baz required but not given"

instance = "abc", bar: "xyz", baz: 123) # => <MyClass#...> # => "abc" # => "definitely not xyz"
instance.baz # => 321


A very simple module that allows a class to implement, which forwards any arguments to a new instance and then calls that instance.


require 'skywalker/callable'

class MyClass
  include Skywalker::Callable

  def initialize(message)
    @message = message

  def call
end"Hello World").call # => Hello World"Hello World 2") # => Hello World 2

Tiny but convenient.


Will include Acceptable.

Implements the core transactional logic used by Skywalker::Command.

Makes call open a transaction, running execute! and calling confirm_success or confirm_failure as appropriate.

For example, see Skywalker::Command documentation above.


  1. Fork it ( )
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Ensure both sets of tests are green (bundle exec rake)
  4. Commit your changes (git commit -am 'Add some feature')
  5. Push to the branch (git push origin my-new-feature)
  6. Create a new Pull Request