State Machines in Ruby

Share this article

Virtual_finite_state_machine_executor_flow_chart
To give them their Sunday name, Finite State Machines (FSMs) are all around us and if we open our eyes long enough you can see them in play when you buy a can of soda, progress through e-commerce shopping carts, pass through electronic turn styles in train stations. Back in my previous life as an Electronic Engineer we would implement FSMs all the time using a spaghetti network of logic gates. And it’s true in the software world we will spaghetti our code with if statements that really should be cleaned up using the State Pattern.

Dissection of a Finite State Machine

There is really nothing to complex when explaining a FSM. The clue really is in the name, we have a system, module, class, machine etc. with a known number of states. In software terms when they become important is when some internal state matters for the behavior of an object. In essence there are three core concepts for FSM, States (duh), Transitions and Events. As an example lets look at a really simple soda machine. The trick as I see it is to identify the correct object as an FSM. At first glance you may be tempted to pick states such as idle, coins inserted, drink dispensed back to idle. This isn’t a good state machine, although it describes the overall behavior what we have done is mix the behavior of a soda machine and soda machine transaction into a single object.

Implementing a State Machine

The best place to start with a known problem in the Ruby world is of course RubyGems. A quick look on Ruby Toolbox and we get a smorgasboard of state machine gems. My personal preference is the aptly named State Machine gem. My reason for choosing this over anything else is a mix of history (previous experience) and its wealth of integrations. Although not tied directly to Rails, it does have ActiveRecord and ActiveModel integration. These integrations mean we can use nice features such as observers, validation and in the ActiveRecord world we get named scopes and database transactions on transitions. It also plays nicely with other database layers such as DataMapper, Sequel and Mongoid. For now we will simply use the state_machine gems ability to add FSM behavior to plain old Ruby objects. If you would imagine the machine will create a new instance of SodaTransaction whose initial state is awaiting_selection. The transition path will follow something along the lines of: awaiting_selection, dispense_soda, complete.
require 'rubygems'
require 'state_machine'

class SodaTransaction

  state_machine :state, initial: :awaiting_selection do
  end

end
We know instinctively the awaiting_selection
state must react to a button press on the front panel. So lets go ahead and implement that.
require 'rubygems'
require 'state_machine'

class SodaTransaction

  state_machine :state, initial: :awaiting_selection do

    event :button_press do
      transition :awaiting_selection => :dispense_soda
    end

  end

end
At this point we can try out our soda transaction in irb. A quick tip I have found is to run the command irb -I . from the path our soda_transaction.rb file resides. This simply starts an irb session with the working directory set at the current path allowing you to simply use require 'soda_transaction' with no other path necessary.
require 'soda_transaction'

sm = SodaTransaction.new
puts sm.state

#=> awaiting_selection

sm.button_press
puts sm.state
#=> dispense_soda
Huzza! We achieve the behavior we want and getting closer to quenching our thirst to boot. One thing that really annoys me about soda machines is when my favorite tipple is out of stock. The machine waits for me to insert my coins and press the button before telling me I’m going to have to settle for Dr. Pepper. I would hate to deprive anyone of the same rage so lets implement that behavior as well.
class SodaTransaction

  attr_accessor :selection

  state_machine :state, initial: :awaiting_selection do

  event :button_press do
    transition :awaiting_selection => :dispense_soda, if: :in_stock?
  end

  end

  def in_stock?
    stock_levels[@selection] > 0
  end

  def stock_levels
    {
      dr_pepper: 100,
      sprite: 10,
      root_beer: 0,
      cola: 8
    }
  end

end
What we have done here is simply placed a guard on the transition for :awaiting_selection to :dispense_soda. By using a simple look up on a hash we check the desired drink is in stock. Obviously, in a real implementation we would want to handle decrementing the stock and delegating this kind of check elsewhere in the application. For now a simple stubbed hash lookup is just fine. But you can see from the example the selection is an instance variable, where is that set? Surely in the event? The best way I have found to allow these additional parameters to be passed to the event is to override the event its self as so:
class SodaTransaction

  state_machine :state, initial: :awaiting_selection do

    event :button_press do
      transition :awaiting_selection => :dispense_soda, if: :in_stock?
    end

  end

  def button_press(selection)
    @selection = selection
    super
  end

  def in_stock?
    stock_levels[@selection] > 0
  end

  def stock_levels
    {
      dr_pepper: 100,
      sprite: 10,
      root_beer: 0,
      cola: 8
    }
  end

end
Because of the way the methods are declared in state machine we can once we have completed any custom actions in or new method we simply call super to pass the call back up the tree to the original event.

On With the Soda

After that slight detour into the bowels of user experience we still need to complete the transaction for the soda. For this we need another event that will fire when the soda can drops from it’s tray. Quite simply we setup this event moving from dispense_soda state to `complete:
class SodaTransaction

  state_machine :state, initial: :awaiting_selection do

    event :button_press do
      transition :awaiting_selection => :dispense_soda, if: :in_stock?
    end

    event :soda_dropped do
      transition :dispense_soda => :complete
    end

  end

  def button_press(selection)
    @selection = selection
    super
  end

  def in_stock?
    stock_levels[@selection] > 0
  end

  def stock_levels
    {
      dr_pepper: 100,
      sprite: 10,
      root_beer: 0,
      cola: 8
    }
  end

end
Running a couple of lines in irb shows that we have successfully dropped a soda for our user to enjoy.
require 'soda_state_machine'

sm = SodaTransaction.new
puts sm.state

#=> awaiting_selection

sm.button_press(:cola)
puts sm.state

#=> dispense_soda

sm.soda_dropped
puts sm.state

#=> complete
This leaves just one thing is bugging us now and thats managing the stock of our soda machine. From the above example we are now one can of cola less.

Observers and Callbacks

Fortunately managing this would be trivial using the state_machine gem. It provides a number of transition callbacks that work similarly to Rails filters. We could set a callback on the :soda_dropped event like so.
class SodaTransaction

  state_machine :state, initial: :awaiting_selection do

    after_transition :on => :soda_dropped, :do => :manage_stock

    event :button_press do
      transition :awaiting_selection => :dispense_soda, if: :in_stock?
    end

    event :soda_dropped do
      transition :dispense_soda => :complete
    end

  end

  def button_press(selection)
    @selection = selection
    super
  end

  def manage_stock
    puts "Removing 1 from the #{@selection} count"
  end

  def in_stock?
    stock_levels[@selection] > 0
  end

  def stock_levels
    {
      dr_pepper: 100,
      sprite: 10,
      root_beer: 0,
      cola: 8
    }
  end

end
As I’m using a simple hash for stock management I’m just printing out that SodaTransaction object is sending out some message to remove 1 from the current stock count. It doesn’t feel quite right though from a design point of view SodaTransaction is now managing the stock of the soda machine. My gut tells me we may want to look at handling this another way. Don’t get me wrong in the simple machine we have implemented what we gave is fine in my opinion. In the case where we need more, such as logging times when more drinks are sold, or recording exits from the purchase funnel using these callbacks with Observers may be more applicable.
class SodaStockObserver

  def self.manage_stock(transaction, transition)
    puts "Removing 1 from the #{transaction.selection} count"
  end

  def self.after_transition(transaction, transition)
    puts "#{transition.attribute} was: #{transition.from}, #{transition.attribute} is: #{transition.to}"
  end

end

class SodaTransaction

  attr_reader :selection

  state_machine :state, initial: :awaiting_selection do

    after_transition :on => :soda_dropped, :do => SodaStockObserver.method(:manage_stock)
    after_transition SodaStockObserver.method(:after_transition)

    event :button_press do
      transition :awaiting_selection => :dispense_soda, if: :in_stock?
    end

    event :soda_dropped do
      transition :dispense_soda => :complete
    end

  end

  def button_press(selection)
    @selection = selection
    super
  end

  def in_stock?
    stock_levels[@selection] > 0
  end

  def stock_levels
    {
      dr_pepper: 100,
      sprite: 10,
      root_beer: 0,
      cola: 8
    }
  end

end
As you can see using an observer on the transition callbacks can be really powerful for not much effort. A note about the observer code is the use of transition.attribute in the after_transition method. This simply represents what key we have given our state machine, in this case “state”. As mentioned earlier the state_machine gem has some lovely integrations with popular database layers such as ActiveRecord. Part of these integrations is ‘automagic’ callbacks. For example if the SodaTransaction object inherited from ActiveRecord then simply defining a SodaTransactionObserver would take care of wiring up for us.
class SodaTransaction < ActiveRecord::Base
  ...
end

class SodaTransactionObserver < ActiveRecord::Observer

  def after_soda_dropped(transaction, transition)
    # Remove 1 from selection qty.
  end

  def after_transition(transaction, transition)
    # Do whatever logging we need
  end

end

Practical Usage

As far as state machines in the wild go there are plenty out there. Projects which spring to mind are Spree, RestfulAuthentication and of a non Ruby flavor EmberJS. Spree uses a FSM in the checkout process, moving from taking various payment details, to order completion. You can even hook into the FSM and inject your own states to further customize the checkout process. RestfulAuthentication (does anyone use this since Devise?) implements a state machine (acts_as_state_machine). It manages the account for a user, ‘pending’, ‘active’, ‘suspended’ etc. Ember on the other hand, although obviously not written in Ruby uses a state machine as the principle of it’s routing. Where as the user navigates through the site the router uses the URI to decide upon the applications state. There was also a point in time where a state machine was integrated into ActiveRecord for a while, but was removed pre 3.0 release. In my experience they are a real ace in the hole for developers and I was surprised about how little they are actually used. That said, FSMs can be implemented in the wrong situations making life miserable in general. I guess the take away advice would be, by all means start out just using an array of states and a state instance variable on your objects. But never be afraid to implement a fuller FSM solution when the former becomes too unwieldy.

Frequently Asked Questions (FAQs) about State Machines in Ruby

What is the significance of state machines in Ruby?

State machines are a crucial aspect of Ruby programming. They provide a way to manage the different states of an object and the transitions between those states. This is particularly useful in applications where objects have distinct states with specific behaviors associated with each state. For instance, in an e-commerce application, an order might have states like ‘new’, ‘processed’, ‘shipped’, and ‘delivered’. Each state has different behaviors and rules associated with it. Using a state machine helps to manage these states and transitions in a clear and organized manner.

How do I implement a state machine in Ruby?

Implementing a state machine in Ruby involves defining the states and transitions for an object. This can be done using the ‘state_machine’ gem. First, you need to install the gem by adding ‘gem “state_machine”‘ to your Gemfile and running ‘bundle install’. Then, in your model, you can define the state machine. Here’s a basic example:

class Order < ActiveRecord::Base
state_machine :initial => :new do
event :process do
transition :new => :processed
end

event :ship do
transition :processed => :shipped
end

event :deliver do
transition :shipped => :delivered
end
end
end

In this example, the ‘Order’ model has a state machine with states ‘new’, ‘processed’, ‘shipped’, and ‘delivered’. The ‘event’ methods define the transitions between states.

Can I use state machines with Rails?

Yes, state machines can be used with Rails. The ‘state_machine’ gem is compatible with Rails and can be used to manage the states of ActiveRecord models. This allows you to define complex state logic in your Rails applications in a clear and organized way.

What are the benefits of using state machines in Ruby?

State machines provide several benefits in Ruby programming. They allow you to manage complex state logic in a clear and organized way. This makes your code easier to understand and maintain. State machines also enforce rules about what transitions are allowed, which can help to prevent bugs and ensure that your application behaves correctly. Additionally, state machines can provide useful features like automatic validation of state transitions, callbacks for state events, and more.

How can I handle state transitions with state machines?

State transitions are handled in state machines using the ‘event’ method. This method defines a transition between states. For example, in an ‘Order’ state machine, you might have an ‘event’ method for ‘process’, ‘ship’, and ‘deliver’. Each of these events represents a transition from one state to another. You can trigger these transitions in your code using the event methods. For example, to transition an order from ‘new’ to ‘processed’, you would call ‘order.process’.

Can I use state machines with other Ruby frameworks?

Yes, state machines can be used with other Ruby frameworks. The ‘state_machine’ gem is compatible with many Ruby frameworks, including Rails, Sinatra, and more. This allows you to use state machines in a variety of Ruby applications.

How can I validate state transitions with state machines?

State machines provide automatic validation of state transitions. This means that if you try to make an invalid transition, the state machine will raise an error. You can also define custom validation rules for your state transitions. This can be done using the ‘before_transition’ and ‘after_transition’ callbacks, which allow you to run custom code before or after a state transition.

Can I use state machines to manage multiple states in an object?

Yes, state machines can be used to manage multiple states in an object. Each state machine manages one state, but an object can have multiple state machines. This allows you to manage complex state logic in a clear and organized way.

How can I handle state events with state machines?

State events are handled in state machines using the ‘event’ method. This method defines a transition between states and can be called to trigger that transition. You can also define callbacks for state events, which allow you to run custom code when a state event occurs.

Can I use state machines in non-Rails Ruby applications?

Yes, state machines can be used in non-Rails Ruby applications. The ‘state_machine’ gem is a standalone library and does not require Rails. This means you can use it in any Ruby application, whether it’s a Rails application, a Sinatra application, or a standalone Ruby script.

Dave KennedyDave Kennedy
View Author

Dave is a web application developer residing in sunny Glasgow, Scotland. He works daily with Ruby but has been known to wear PHP and C++ hats. In his spare time he snowboards on plastic slopes, only reads geek books and listens to music that is certainly not suitable for his age.

Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week
Loading form