State Machine
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.