This is a blog of AmberBit - a Ruby on Rails web development company. Hire us for your project!

State Machines

Problem

When you’re working on an application you deal with many types of objects. For some of these objects it feels natural to think of them as being in one of a few possible states.

Let’s take a simple Account model as an example. When the user registers in your application, new Account is created and it’s put into new state. After registration user can request verification email to be sent, which results in changing Account’s state to unverified. Once the user clicks on the verification link, the Account becomes verified. If the user behaves badly, his Account might get suspended. Let’s also assume that the Account can be deleted, but since we don’t want to loose any data we implement soft delete pattern by simply changing the state to deleted. It means that at any given time, the Account can be in one of the following five states: new, unverified, verified, suspended and deleted.

Storing current state doesn’t look like a particularly difficult problem to solve. You can store it as a string or, better yet, use enum for this purpose. Just define a list of accepted statuses in your model and you’re good to go.

class Account < ActiveRecord::Base
  enum status: [:new, :unverified, :verified, :suspended, :deleted]
  # ...
end

However, sometimes things are more complex than this. You might want to allow transitions only between some of the states. In the above example, transition to unverified might only be possible from the new and suspended state. We might also want to require user to repeat the verification process after suspended account becomes active again. We can achieve it by disabling direct transition from suspended to verified state. In addition, you might need to check some additional conditions before changing status, such as verifying credit card before activating suspended user.

It’s clear that a string or enum attribute alone is not enough. We need a mechanism that will help us enforce above constraints. One of the tools that might be helpful is state machine, or more precisely finite-state machine (FSM for short).

This article will introduce you to the concept of state machines.

What are State Machines?

State Machine is a mathematical model of computation, used primarily for designing algorithms and electronic circuits. The diagram below shows a very simple state machine describing light bulb. The major components of state machine are captioned.

Simple state machine

At any point in time, the state machine is in one of finite number of states. When state machine receives an input it may switch to a different state. You can think of state machine as a flow chart. The act of moving from one state to another is called transition. For the transition to occur, a transition condition must be met.

Slightly more complex diagram, illustrating the Account’s state machine as described in the introduction, is shown below:

Account state machine

State Machines in Ruby

If above description caught your attention and you would like to try using state machines in one of your applications, I have a good news for you. You don’t need to roll up your sleeves and start implementing state machine from scratch. As (almost) always, open source community has got you covered. There’s plenty of gems that you can use: State Machine, AASM or Statesman, to name a few popular choices. In the next section of this article, we will investigate the last gem from the list - the Statesman.

Statesman

Statesman is one of popular choices when it comes to state machines in Ruby. It has a few features that make it and interesting option. To mention some of them:

  • it decouples state machine logic from the model; state machine is defined in a separate class as opposed to adding the FSM-related logic into the model itself
  • state transitions are modeled as class and can be persisted in the database; this is useful when you want to keep the history of state changes
  • transition metadata - transitions can contain unstructured metadata; this metadata can be persisted together with the transition and included in the audit history

Let’s implement the state machine for our Account model. As I mentioned above, the state machine lives in a separate class. This is great, as it will help us maintain separation of concerns between the model and state machine.

class AccountStateMachine
  include Statesman::Machine

  state :new, initial: :true
  state :unverified
  state :verified
  state :suspended
  state :deleted

  transition from: :new, to: [:unverified, :deleted]
  transition from: :unverified, to: [:verified, :deleted]
  transition from: :verified, to: [:suspended, :deleted]
  transition from: :suspended, to: [:unverified, :deleted]
end

The code is pretty straightforward. We list all the possible states in which the Account can be, decide which will be the initial one and define all accepted transitions.

Now, let’s also define a basic Transition model:

class AccountTransition < ActiveRecord::Base
  include Statesman::Adapters::ActiveRecordTransition

  belongs_to :account
end

Last thing we need to do is making a few changes to the Account model itself. We’ll define state machine and transition classes and the initial state:

class Account < ActiveRecord::Base
  has_many :account_transitions

  def state_machine
    @state_machine ||= AccountStateMachine.new(self, transition_class: AccountTransition)
  end

  private

  def self.transition_class
    AccountTransition
  end

  def self.initial_state
    :new
  end
end

That’s it, we’ve set up our first state machine! We can now access it by calling state_machine method on the Account model. It has a bunch of useful methods that we can use for checking the state, as well as validating and triggering transitions. Let’s see them in action:

account = Account.find(1)

# check current state
account.state_machine.current_state # => "new"

# trigger transition
account.state_machine.transition_to!(:unverified) # => true
account.state_machine.current_state # => "unverified"

# check if transition is valid
account.state_machine.can_transition_to?(:new) # => false

So far so good. Let’s extend our state machine with some more advanced features. In the current implementation, it’s possible to change one state to another as long as the transition is defined in the state machine. In many cases it’s necessary to introduce additional constraints. We can achieve this through the so-called guards. Defining guards is pretty straightforward:

class AccountStateMachine
  # ...

  guard_transition(from: :unverified, to: :verified) do |account|
    account.valid_credit_card?
  end
end

Guards should return either true or false. If the latter is returned, transition will not succeed.

Another useful feature is the ability to define callbacks that will be executed either before or after the transition. The syntax is very similar to guards:

class AccountStateMachine
  # ...

  before_transition(from: :unverified, to: :verified) do |account, account_transition|
    account.generate_verification_code!
  end

  after_transition(to: :unverified) do |account, account_transition|
    AccountMailer.verification_code(account).deliver
  end
end

One last thing we’re going to do is enabling transitions history persistence. By default it’s only stored in memory. To change it, we need to configure the Statesman to use different adapter. Let’s create config/initializers/statesman.rb file with following content:

# config/initializers/statesman.rb

Statesman.configure do
  storage_adapter(Statesman::Adapters::ActiveRecord)
end

Statesman provides generator, which will create a database migration for transition table. Its called with the following command:

rails g statesman:active_record_transition Account AccountTransition

and the generated migration looks like this:

class CreateAccountTransitions < ActiveRecord::Migration
  def change
    create_table :account_transitions do |t|
      t.string :to_state, null: false
      t.text :metadata, default: "{}"
      t.integer :sort_key, null: false
      t.integer :account_id, null: false
      t.boolean :most_recent, null: false
      t.timestamps null: false
    end

    add_index(:account_transitions,
              [:account_id, :sort_key],
              unique: true,
              name: "index_account_transitions_parent_sort")
    add_index(:account_transitions,
              [:account_id, :most_recent],
              unique: true,
              where: 'most_recent',
              name: "index_account_transitions_parent_most_recent")
  end
end

For a more in-depth introduction to Statesman, be sure to check out their github repository.

Closing words

I hope that I’ve managed to convince you that state machines can be very powerful addition to your toolset. However, it’s important to remember that the choice of tool should always be dictated by the problem you’re trying to solve. State machines, when misused, can increase the code complexity instead of reducing it. It is therefore important to weigh the complexity of all possible options before picking the right tool for the job. You shouldn’t automatically assume that every time your model has a state attribute, it must be implemented with state machine. Sometimes simpler solutions are better.

by Kamil Bielawski

Do you need skilled professionals to help you build Rails applications? Hire us for your project!
comments powered by Disqus

Want to get in touch? Drop us a line!