DEV Community

Davide Santangelo
Davide Santangelo

Posted on

A Deep Dive into the Statesman Gem for Ruby: Building Flexible State Machines

State management is an integral part of many applications, especially when dealing with workflows, order statuses, or user lifecycle management. A state machine helps manage the transitions between various states in a well-defined, predictable manner. While there are many gems available for implementing state machines in Ruby, one of the most powerful and flexible is the statesman gem. Unlike other state machine gems, statesman focuses on flexibility, database-backed transitions, and decoupled state logic.

In this article, I’ll walk you through the use of statesman, explain how to implement a state machine, and explore the more advanced features such as callbacks and persistence in the database.

Why Use Statesman?

Statesman was developed by the team GoCardless to address some of the limitations in other state machine gems like AASM or state_machine. It offers:

  1. Database-backed state transitions: You can keep track of each state change in a database, which makes it easy to debug, audit, or rollback transitions.
  2. Decoupled state management: The logic is kept separate from the model, making it more maintainable.
  3. Flexibility: You can define state transitions, guard clauses, and callbacks without polluting your models.

Setting Up Statesman

Let’s start by installing and setting up the gem in your project.

Installation

Add statesman to your Gemfile:

gem 'statesman'
Enter fullscreen mode Exit fullscreen mode

Run:

bundle install
Enter fullscreen mode Exit fullscreen mode

Now, you’re ready to use the gem.

Basic Usage

The simplest way to get started with statesman is by creating a transition class and a state machine class.

Step 1: Create a Model

For this example, let’s assume you’re working on an e-commerce platform, and you want to manage the state transitions of an Order.

class Order < ApplicationRecord
  has_many :order_transitions, autosave: true

  # Initialize the state machine
  def state_machine
    @state_machine ||= OrderStateMachine.new(self, transition_class: OrderTransition)
  end
end
Enter fullscreen mode Exit fullscreen mode

Step 2: Define the Transition Class

The transition class will store each state change in the database. Let’s create the OrderTransition model.

rails g model OrderTransition to_state:string metadata:jsonb sort_key:integer order:references
Enter fullscreen mode Exit fullscreen mode

Then, update the migration to index the order_id and sort_key columns:

class CreateOrderTransitions < ActiveRecord::Migration[6.1]
  def change
    create_table :order_transitions do |t|
      t.string :to_state, null: false
      t.jsonb :metadata, default: {}
      t.integer :sort_key, null: false
      t.references :order, foreign_key: true
      t.timestamps
    end

    add_index :order_transitions, :sort_key
    add_index :order_transitions, [:order_id, :sort_key], unique: true
  end
end
Enter fullscreen mode Exit fullscreen mode

This migration creates a table that records each state change for an order. The sort_key field ensures the transitions are ordered chronologically, and the metadata field can store any additional information related to the transition.

Step 3: Define the State Machine Class

The next step is to define the actual state machine. This class is responsible for defining states, transitions, and any callbacks that should occur when transitioning between states.

class OrderStateMachine
  include Statesman::Machine

  state :pending, initial: true
  state :processing
  state :shipped
  state :completed
  state :canceled

  transition from: :pending, to: [:processing, :canceled]
  transition from: :processing, to: [:shipped, :canceled]
  transition from: :shipped, to: :completed

  # Callbacks
  before_transition(from: :pending, to: :processing) do |order, transition|
    order.validate_payment!  # Custom logic before transition
  end

  after_transition(to: :shipped) do |order, transition|
    order.send_shipping_confirmation!  # Custom logic after transition
  end

  guard_transition(from: :processing, to: :shipped) do |order|
    order.ready_to_ship?  # Only allow transition if the order is ready to ship
  end
end
Enter fullscreen mode Exit fullscreen mode

Here’s a breakdown of what’s happening:

  • We define our states: pending, processing, shipped, completed, and canceled.
  • Transitions are defined, ensuring that you can only move between certain states in a controlled way.
  • We define two callbacks:
  • A before_transition callback to validate payment before an order transitions from pending to processing.
  • An after_transition callback to send a shipping confirmation once the order is marked as shipped.
  • A guard_transition ensures that an order can only transition from processing to shipped if it meets the ready_to_ship? condition.

Working with State Machines

Once your state machine is set up, interacting with it is straightforward. Here’s an example of how you might use the state machine in the Order model.

order = Order.find(1)

# Check the current state
order.state_machine.current_state
# => "pending"

# Trigger a transition
order.state_machine.transition_to(:processing)

# Check if a transition is allowed
order.state_machine.can_transition_to?(:shipped)
# => false

# Add metadata during a transition
order.state_machine.transition_to(:processing, metadata: { user_id: current_user.id })
Enter fullscreen mode Exit fullscreen mode

Database-backed State Transitions

By default, statesman does not store the state of the object in the model itself but instead relies on the transition table. To access the current state, you can use statesman’s built-in functionality:

order.state_machine.current_state
# or
OrderStateMachine.current_state(order)
Enter fullscreen mode Exit fullscreen mode

If you need the current state to be stored in the model (e.g., for performance reasons), you can add a current_state column to your model and keep it in sync via callbacks.

rails g migration AddCurrentStateToOrders current_state:string
Enter fullscreen mode Exit fullscreen mode
class Order < ApplicationRecord
  has_many :order_transitions, autosave: true

  after_initialize :set_initial_state, if: :new_record?

  def set_initial_state
    self.current_state ||= "pending"
  end

  def state_machine
    @state_machine ||= OrderStateMachine.new(self, transition_class: OrderTransition, association_name: :order_transitions)
  end
end
Enter fullscreen mode Exit fullscreen mode

Then update the OrderStateMachine class to keep this column in sync:

class OrderStateMachine
  include Statesman::Machine

  after_transition do |order, transition|
    order.update!(current_state: transition.to_state)
  end
end
Enter fullscreen mode Exit fullscreen mode

Now, every time a state transition occurs, the current_state column in the orders table will be updated automatically.

Advanced Features

Reverting a State

If a transition goes wrong, you might need to rollback to a previous state. Since statesman keeps all transitions in a database table, reverting is easy:

last_transition = order.order_transitions.last
order.state_machine.transition_to(last_transition.from_state)
Enter fullscreen mode Exit fullscreen mode

Querying Transitions

You can also query transitions to find out when a certain state change occurred or who triggered the change:

order.order_transitions.where(to_state: 'shipped').first
Enter fullscreen mode Exit fullscreen mode

Custom Transition Metadata

As shown earlier, you can pass metadata when performing a transition. This is useful for storing additional context (e.g., who approved an order, what was the shipping provider, etc.):

order.state_machine.transition_to(:shipped, metadata: { shipped_by: 'FedEx' })
Enter fullscreen mode Exit fullscreen mode

Conclusion

The statesman gem is a powerful, flexible tool for managing state transitions in your Ruby applications. By using statesman, you not only separate the state machine logic from your models but also gain the ability to track state changes in the database, run callbacks, and enforce guards during transitions.

In this guide, we’ve covered how to:

  • Set up statesman and integrate it into your Rails app.
  • Define states, transitions, guards, and callbacks.
  • Track state transitions in the database and even store metadata.

Whether you’re managing order states in an e-commerce app or handling complex workflows in an enterprise system, statesman is a great choice for building robust, maintainable state machines in Ruby.

Top comments (1)

Collapse
 
w3ndo profile image
Patrick Wendo

Well written. I can already think of a few use cases for this. Will definitely consider it for my next pet project