State Machines in Ruby
State machines are one of the most useful design patterns in application development. They let you model the lifecycle of an object with precision — defining exactly which states it can be in, which transitions are allowed, and what happens when a transition fires.
This guide covers the AASM gem (formerly known as acts_as_state_machine), the de facto standard for state machines in Ruby.
What Is a State Machine?
A state machine describes how an object moves through a finite set of states. Each state represents a condition or mode the object can be in. Transitions are the allowed movements between states, triggered by events.
A classic example is an order in an e-commerce system:
[pending] --pay--> [paid] --ship--> [shipped] --deliver--> [delivered]
|
+--cancel--> [cancelled]
Without a state machine, you’d likely scatter status checks and assignments across your codebase. With one, the valid paths are enforced in one place.
Installing AASM
Add the gem to your Gemfile:
gem 'aasm'
Then install it:
bundle install
Defining States and Events
Here’s a minimal AASM example with a Ticket class:
require 'aasm'
class Ticket
include AASM
attr_reader :id, :title
def initialize(id, title)
@id = id
@title = title
end
aasm do
state :open, initial: true
state :in_progress
state :resolved
state :closed
event :start do
transitions from: :open, to: :in_progress
end
event :resolve do
transitions from: :in_progress, to: :resolved
end
event :close do
transitions from: [:resolved, :in_progress], to: :closed
end
event :reopen do
transitions from: [:closed, :resolved], to: :open
end
end
end
The aasm block declares all states and events. open is the initial state, meaning new instances start there automatically.
Transitions
Transitions map events to state changes. The transitions method accepts:
from:— the source state (or array of states)to:— the destination stateguard:— a condition that must be true for the transition to succeed (more on this below)after:— a callback that runs after the transition completes
event :resolve do
transitions from: :in_progress, to: :resolved
end
Multiple source states are supported:
event :close do
transitions from: [:resolved, :in_progress], to: :closed
end
Guards (Conditions on Transitions)
Guards let you conditionally allow a transition. Pass a lambda or a method name:
class Order
include AASM
attr_accessor :payment_received
aasm do
state :pending, initial: true
state :paid
state :shipped
state :delivered
event :mark_paid, guards: :payment_received? do
transitions from: :pending, to: :paid
end
event :ship do
transitions from: :paid, to: :shipped
end
event :deliver do
transitions from: :shipped, to: :delivered
end
end
def payment_received?
payment_received == true
end
end
order = Order.new
order.mark_paid? # => false
order.payment_received = true
order.mark_paid # transitions to :paid
order.mark_paid? # => true
If a guard returns false, the transition is rejected and the object stays in its current state. You can check may_mark_paid? to ask whether the transition is currently allowed without triggering it.
Callbacks
AASM provides callbacks that run at specific points around transitions:
| Callback | When it runs |
|---|---|
before | Before the transition executes |
after | After the transition completes |
after_all_transactions | After all database transactions commit |
before_all_transactions | Before any transaction opens |
ensure | Always, regardless of outcome |
error | If an exception is raised during the transition |
class Document
include AASM
attr_accessor :content, :version
aasm do
state :draft, initial: true
state :review
state :published
event :submit_for_review do
transitions from: :draft, to: :review
end
event :publish do
transitions from: :review, to: :published,
after: :increment_version
end
event :revise do
transitions from: :published, to: :draft
end
end
private
def increment_version
self.version ||= 0
self.version += 1
end
end
doc = Document.new
doc.version # => nil
doc.submit_for_review
doc.publish
doc.version # => 1
You can also attach callbacks directly with before, after, and ensure inside the event block:
event :archive do
transitions from: [:published, :draft], to: :archived,
before: :send_archive_notification,
after: :cleanup_assets
end
Auto-Created Methods
When you define a state machine, AASM generates a set of useful methods on your class:
Event methods (trigger transitions):
ticket.start # fires the start event
ticket.start! # fires and raises error on failure (bang version)
ticket.may_start? # returns true if the transition is currently allowed
ticket.started? # predicate — true if the current state is :started
Bang methods raise an AASM::InvalidTransition if the transition is not allowed. Non-bang versions silently do nothing when the transition cannot be made.
State predicates:
ticket.open? # true if in :open state
ticket.in_progress? # true if in :in_progress state
ticket.closed? # true if in :closed state
Transition history:
ticket.aasm.current_state # => :open
ticket.aasm.states # => [#<AASM::State>, ...]
ticket.aasm.events # => [#<AASM::Event>, ...]
Integrating with ActiveRecord
AASM works especially well with ActiveRecord models. The gem handles transactions automatically and persists the state as a column.
First, add a state column to your migration:
class AddStateToOrders < ActiveRecord::Migration[7.0]
def change
add_column :orders, :state, :string, default: 'pending', null: false
add_index :orders, :state
end
end
Then use AASM in your model:
class Order < ApplicationRecord
include AASM
aasm column: :state do
state :pending, initial: true
state :paid
state :shipped
state :delivered
state :cancelled
event :pay, guards: :payment_valid? do
transitions from: :pending, to: :paid
end
event :ship do
transitions from: :paid, to: :shipped
end
event :deliver do
transitions from: :shipped, to: :delivered
end
event :cancel do
transitions from: [:pending, :paid], to: :cancelled
end
end
private
def payment_valid?
payment_token.present? && amount_cents > 0
end
end
AASM will automatically persist the new state after a successful transition. If you need to handle persistence manually or conditionally, you can disable automatic saving:
aasm column: :state, skip_validation_on_save: true do
# ...
end
To use a non-default column name, pass the column: option as shown above.
Whiny Mode
By default, AASM raises AASM::InvalidTransition when you try to fire an unavailable event. You can disable this “whiny” behaviour:
aasm whiny_transitions: false do
# ...
end
With whiny mode off, invalid transitions silently no-op.
Visualising State Machines
For complex machines, a visual diagram is invaluable. You can generate a Graphviz DOT representation using the aasm_graph gem or generate it manually:
puts Order.aasm.state_machine.graph.output
This produces DOT output you can render with Graphviz to get a PNG or SVG of your state diagram.
Alternatively, tools like Mermaid can render state diagrams from a simple text description:
stateDiagram-v2
[*] --> pending
pending --> paid : pay
paid --> shipped : ship
shipped --> delivered : deliver
pending --> cancelled : cancel
paid --> cancelled : cancel
When to Use a State Machine
A state machine is the right tool when:
- An object has a finite, well-defined set of states
- Transitions between states have business rules attached to them
- You want to enforce valid paths and prevent invalid ones
- You need auditability — knowing what state something is in and how it got there
- Your domain has concurrency concerns — AASM’s transaction support helps here
Signs you might need a state machine:
# Without a state machine — easy to put the object in a bad state
order.status = "delivered"
order.status = "pending" # wait, that shouldn't be allowed
# With a state machine — invalid transitions are impossible
order.deliver # valid: shipped -> delivered
order.pending! # raises AASM::InvalidTransition
Common use cases include:
- Order and payment processing pipelines
- Job or task status tracking
- Subscription and billing lifecycles
- Document approval workflows
- Server or deployment state
AASM vs Other Options
AASM is the most widely-used state machine gem in the Ruby ecosystem. Alternatives worth knowing:
state_machines— a more Rails-integrated alternative (now largely unmaintained)workflow— a lighter, simpler option- Roll your own — for very simple cases, a
statuscolumn and a few scopes may be enough
AASM strikes the best balance of features, flexibility, and active maintenance for most applications.
See Also
- /guides/ruby-command-pattern/ — encapsulate operations as objects, a natural companion to state machine events
- /guides/ruby-decorators-pattern/ — layer behaviour on top of objects without modifying their class
- /guides/ruby-service-objects/ — extract complex business logic into dedicated service classes