Back in college, I recall my professor used a vending machine as a classic example of a state machine, and this was taught in CS2100. At the time, I found it mind-numbingly trivial - just another academic concept that seemed disconnected from real-world examples (other than the vending machine).

Over the years, I had my first encounter with state machines in the workplace. In one of my internship stints, I had the opportunity to define my own state machine workflows with AWS Step Functions to update a cost dashboard.

Recently, I learnt about AASM, which provides a convenient way to define state machines for Ruby classes. Complex applications are essentially a series of state transitions. For example, e-commerce platforms track orders from pending to processing to shipped. Or background job systems navigating through scheduled, processing, and completed states. Modelling these business objects as state machines comes as a sensible solution.

Take the example of a BlogPost model with different states allowed for the column state. We can define behaviours with callbacks for each transition. e.g. check_submission_criteria before transitioning from draft to reviewing, or run_publishing_chores after publishing. This helps clear up scattered conditional logic and complex state management.

class BlogPost < ActiveRecord::Base
  include AASM

  aasm column: 'state' do
    state :draft, initial: true
    state :reviewing
    state :published
    state :archived

    event :submit_for_review do
      transitions from: :draft, to: :reviewing, guard: :check_submission_criteria
    end

    event :publish do
      transitions from: :reviewing, to: :published, after: :run_publishing_chores
    end

    event :archive do
      transitions from: [:draft, :reviewing, :published], to: :archived
    end
  end

  private

  def check_submission_criteria
    # do some checks 
  end

  def run_publishing_chores
    # send email notifications to subscribers, update records
    # update author's record
    # do other things
  end
end

AASM isn’t just syntactic sugar. It provides three key benefits:

  • Simplicity: AASM defines the state and transitions in a clear and readable manner.

    irb(main):010:0> post.draft?
    => true
    irb(main):011:0> post.submit_for_review!
    TRANSACTION (3.7ms)  begin transaction
    BlogPost Update (4.0ms)  UPDATE "blog_posts" SET "updated_at" = ?, "state" = ? WHERE "blog_posts"."id" = ?  [["updated_at", "2023-07-15 07:45:49.092067"], ["state", "reviewing"], ["id", 1]]
    TRANSACTION (0.9ms)  commit transaction
    => true
    irb(main):012:0> post.draft?
    => false
    irb(main):013:0> post.reviewing?
    => true
    
  • Validation: AASM helps mitigate invalid state transitions.

    irb(main):015:0> post1.draft?
    => true
    irb(main):016:0> post1.publish! # error! invalid transition
    /Users/kelvinharris/.rbenv/versions/3.1.4/lib/ruby/gems/3.1.0/gems/aasm-5.5.0/lib/aasm/aasm.rb:202:in `aasm_failed': Event 'publish' cannot transition from 'draft'. (AASM::InvalidTransition)
    
  • Persistence: AASM can be easily integrated with ActiveRecord (which supports most databases) to persist the state of models.

    sqlite> SELECT * FROM blog_posts WHERE id = 1;
    id|title|created_at|updated_at|state|body
    1|State Machine|2023-07-15 05:19:55.395120|2023-07-15 05:19:55.395120|draft|Body
    sqlite> SELECT COUNT(*) FROM blog_posts WHERE state = 'archived';
    5
    

By defining explicit rules, you are also forcing your application to behave predictably, so you won’t get any more surprises with accidental transitions.