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:
- 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.
- Decoupled state management: The logic is kept separate from the model, making it more maintainable.
- 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'
Run:
bundle install
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
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
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
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
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 })
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)
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
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
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
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)
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
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' })
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.