ruby

# State Machines in Ruby: An Introduction

Pulkit Goyal on

A state machine can hold all possible states of something and the allowed transitions between these states. For example, the state machine for a door would have only two states (open and closed) and only two transitions (opening and closing).

On the other hand, complex state machines can have several different states with hundreds of transitions between them. In fact, if you look around, you will notice finite state machines surrounding you — when you buy from a website, get a pack of crisps from the vending machine, or even just withdraw money from an ATM.

In this post, we'll look at how to set up a state machine in Ruby and use the state machines gem.

## State Machines in Development

When do we need a state machine in development? The simple answer is whenever you want to model multiple rules for state transitions or perform side-effects on some transitions. The key here is to identify parts of your application that would benefit from a state machine. A good example that always works for me is an Order in the context of an e-commerce application.

For a simple application selling products online, an Order can be in one of several states:

• created
• processing
• ready
• shipped
• delivered
• void

You can see the allowed transitions in the following diagram.

Visualizing the order in the form of a state machine immediately allows us to clearly understand the full flow, from ordering the product to delivery. And for us developers, it enables clear, set steps on what can and cannot be done at particular points in that flow.

That being said, it is easy to jump down a rabbit hole by picking this up for a very wide problem and then end up with hundreds of states that are difficult to reason about and follow through. So always have a top-level idea and a state chart for your proposed state machine before implementing it.

## Our First State Machine in Ruby

Let's try to implement our OrderStateMachine with Ruby.

class OrderStateMachine
def initialize(order)
@order = order
@order.state = :created if @order.state.blank?
end

def mark_as_paid!
raise "Invalid state #{@order.state}" unless @order.created?

@order.state = :processing
end

def packed!
raise "Invalid state #{@order.state}" unless @order.processing?

@order.state = :ready
end

def shipped!
raise "Invalid state #{@order.state}" unless @order.ready?

@order.state = :shipped
end

# ...
end

That's a simple enough implementation. For each possible transition in the state machine, we define a new method that does some sanity checks and performs the transition.

The great thing about the above implementation is that everything is explicit: any new developer can very quickly understand the full extent of the state machine.

Let's see how we can add some side-effects to the transitions. We'll automatically ship orders that are packed and deliverable:

class OrderStateMachine
# ...

def packed!
raise "Invalid state #{@order.state}" unless @order.processing?

@order.state = :ready
ship_order if @order.deliverable?
end

# ...

private

def ship_order
DeliveryService.create_consigment!(@order)
shipped!
end
end

While a naïve implementation works great, it is overly verbose, especially when we have a lot of transitions and conditions.

A quick check on Ruby Toolbox brings up several gems for state machines. state_machines and aasm are the most popular ones, and both come with an ActiveRecord adapter if you want to use them with Rails. Both are thoroughly tested and production-ready, so do check them out if you need to implement a state machine.

## Using the State Machines Gem in Ruby

For this post, I will describe how we can model the state machine for our Order using the state_machines gem.

class Order
state_machine :state, initial: :created do
event :confirm_payment do
transition created: :processing
end
event :pack do
transition processing: :ready
end
event :cancel do
transition %i[created processing ready] => :void
end
event :return do
transition delivered: :void
end
event :ship do
transition ready: :shipped
end
event :fail_delivery do
transition shipped: :processing
end
end
end

The above class defines our simple state machine and all of its possible transitions. On top of this, it also automatically exposes a lot of utility methods on an order:

order.state           # => "created"
order.state_name      # => :created
order.created?        # => true
order.can_pack?       # => false (since payment has not been confirmed yet)
order.confirm_payment # => true (and transitions to processing)
order.can_pack?       # => true

## Execute Side-Effects with the State Machines Gem in Ruby

As is usual for any real-world system, many transitions in a state machine will come with side-effects. The state_machines gem makes it easy to define and execute them. Let's add two side-effects:

1. On all transitions to ready, create a delivery consignment if the order is deliverable.
2. On all fail_delivery events, send an email to the user notifying them of the event.
class Order
attr_accessor :deliverable

state_machine :state, initial: :created do
# ...

after_transition to: :ready, do: :create_delivery_consignment, if: :deliverable
after_transition on: :fail_delivery, do: :notify_delivery_failure
end

private

def create_delivery_consignment
DeliveryService.create_consigment!(self)
ship
end

def notify_delivery_failure
UserMailer.delivery_failure_email(self).deliver_later
end
end

We can perform transitions as usual in the order:

order.deliverable = true
order.pack            # => true
order.state           # => "shipped" (through side-effect)

I would suggest checking out the Github page for the state_machines gem to learn about all the possible options the gem offers.

## Use the State Machines Gem with ActiveRecord in Ruby on Rails

As discussed before, both state_machines and aasm support ActiveRecord. When you use the state_machines gem with ActiveRecord, not a lot of things change with the implementation. You can continue using most of what we've already discussed in the previous sections of this post.

Here are some additional features that you get:

1. Side-effects happen inside a transaction. This means that if you do some database operations inside transition hooks and the transaction fails, the state change won't be committed. See use_transactions if you want to disable this behavior.
2. Like all other ActiveRecord callbacks, if you want to update the attributes of the current model from a transition, you must do this inside a before_transition callback. To update attributes inside after_transition, you need to save the model again. Check out the following example:
class Order < ActiveRecord::Base
state_machine initial: :created do

before_transition any => :processing do |order|
# You can just update the attribute here and it will be saved.
order.processing_start_at = Time.zone.now
end

after_transition any => :shipped do |order|
# Call update! to update the order.
# Just setting the attribute like above would not work here.
order.update!(shipped_at: Time.zone.now)
end
end
end
1. The gem automatically provides scopes to filter models by their state. For example, you can do Order.with_state(:processing) to find all the processing orders or Order.without_state(:processing) to find all orders that are not under processing.
2. If a state change is attempted without a matching transition (for example, from processing to delivered), the record will fail to save, and an error will be added to the validation errors. To internationalize the generated error messages, just add some keys to provide translations for states and events inside the config files (for example, en.yml):
en:
activerecord:
state_machines:
order:
states:
created: Created
processing: Under Processing
# ...
events:
return: Return Order
# ...

## Wrap Up: Build State Machines in Ruby

In this post, we explored why we'd use a state machine in development, before building a simple state machine. Finally, we looked at using the state machines gem in Ruby and Ruby on Rails.

Now that you have a basic understanding of a state machine, I am sure you will recognize several scenarios around you that already use a state machine or will benefit greatly from one.

Until next time, I wish you luck building state machines!

P.S. If you'd like to read Ruby Magic posts as soon as they get off the press, subscribe to our Ruby Magic newsletter and never miss a single post!

# Pulkit Goyal

Our guest author Pulkit is a senior full-stack engineer and consultant. In his free time, he writes about his experiences on his blog.

-> All articles by Pulkit Goyal-> Become an AppSignal author

# AppSignal monitors your apps

AppSignal provides insights for Ruby, Rails, Elixir, Phoenix, Node.js, Express and many other frameworks and libraries. We are located in beautiful Amsterdam. We love stroopwafels. If you do too, let us know. We might send you some!