State Machines in Ruby

· 6 min read · Updated April 1, 2026 · intermediate
ruby state-machine design-patterns gems guides

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 state
  • guard: — 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:

CallbackWhen it runs
beforeBefore the transition executes
afterAfter the transition completes
after_all_transactionsAfter all database transactions commit
before_all_transactionsBefore any transaction opens
ensureAlways, regardless of outcome
errorIf 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 status column and a few scopes may be enough

AASM strikes the best balance of features, flexibility, and active maintenance for most applications.

See Also