Last time I heard ‘state machine’ was 5 years back when I was dozing in my computer science lecture. Professor drew some flowchart with math notations on board to explain us the concept of computation around finite states.
State machine aka finite state machine is a system consisting of finite number of states, rules to map one state to another or itself for any possible inputs. The system is in one state at a time and can transition to other allowed state by triggering an event.
As a rails developer, you won’t easily encounter state machine until you find it being used in someone else’s code or some gem. Few weeks back, I was reading through ‘Mastering Modern Payments’ to integrate Stripe in my Rails app and I found the concept being used there. Its one of the best things we could use to design a model having multiple states. We won’t be diving into what is being abstracted away and focus more on ‘when to use them and how can we use them in our rails app’.
Every modern application feature is built around a flow of events. For instance, if we consider ‘ordering a product’ on an ecommerce application, the most common and minimal flow of events would be as follows.
- User places an order.
- Warehouse team processes the order.
- Packing team packs the orderered products.
- Shipping team ships the order to nearest hub.
- Delivery guys deliver the ordered products to user.
- At any point of time before delivery, user can cancel the order.
Representing above flow in a diagram
At any given time, our entity ‘order’ will be under one state and can transition to other permissible states on triggering required event.
So why should I use state machine to design this in Rails?
I heard you.
Ofcourse, you can design above model without using state machine. You could just write some validations to make it work. Its ok to do that as long as your model is limited to a couple of states. But imagine writing the same for a model that has 5-6 different states like our Order model. You would end up beefing up your model with 100 lines of validations just to check the validity of state transitions. If you need to add one more state in your model later, you would again need to go through all the code to check if adding a state could pop any issues. Ultimately, this approach becomes cumbersome to manage and is more prone to bugs.
The main reason for using state machine is to help the design process. It is much easier to figure out all the possible edge conditions by drawing out the state machine on paper. This will make sure that your application will have less bugs and less undefined behavior.
Using state machine in ActiveRecord model.
We will be using this awesome gem called AASM (Act As State Machine). Its a generic library that provides adapters to support various models. We will just be dealing with ActiveRecord model in this post.
Usage
Install the gem as instructed under documentation. Lets design our Order model.
class Order < ActiveRecord::Base
include AASM
aasm column: 'state' do
state :placed, :initial => true
state :processing
state :packed
state :shipped
state :delivered
state :cancelled
event :process do
transitions :from => :placed, :to => :processing
end
event :pack do
transitions :from => :processing, :to => :packed
end
event :ship do
transitions :from => :packed, :to => :shipped
end
event :deliver do
transitions :from => :shipped, :to => :delivered
end
event :cancel do
transitions :from => [:placed, :processed, :packed, :shipped], :to => :cancelled
end
end
end
Whoa! There we go. By just looking at this code, we can picture the flow of our model. It is self explanatory. We have defined our states, events and transitions as per our design. Now lets explore the awesomeness this gem provides out of the box.
It provides us with nifty instance methods.
o = Order.new # new order obj with status as 'placed'
o.placed? # returns true
o.processing? # returns false
o.may_process? # returns truthy value
o.may_cancel? # returns truthy value
o.may_deliver? # returns false
o.process # transitions state to 'processing' but does not save it
o.process! # transitions state to 'processing' and saves it
o.deliver # raises AASM::InvalidTransition: Event 'deliver' cannot transition from 'processing'
Ain’t you already drooling?
If you dont want to raise exceptions, you could do
aasm :whiny_transitions => false do
#...
end
and it will return false
instead of raising exception.
We can also pass a block to state changing methods (events). Block will only execute if transition succeeds.
o.ship! do
o.user.order_shipped_email
end
We can also define callbacks around states, events and transitions. They will be called when certain criterias are met (eg leaving/entering a state)
state :processing, :before_enter => :do_blabla
event :deliver do
after do
# called after transition 'shipped' to 'delivered' is finished
end
transitions :from => :shipped, :to => :delivered
end
You can check the available callbacks and their order of execution documented here.
We can also use guards (:guard, :if, :unless) to transition states conditionally.
event :cancel do
transitions :from => [:placed, :processed, :packed, :shipped], :to => :cancelled, :if => :can_cancel_order?
end
AASM also provides us with scope methods to query required states
Order.shipped # returns all records with state as shipped
Order.placed.where('created_at >= ?', 30.days.ago)
If you are using Rails version 4.1+, chances are high that you are using ActiveRecord Enums.
Good news is AASM plays well with Enums.
enum state:{
placed: 0,
processing: 1,
packed: 2,
shipped: 3,
delivered: 4,
cancelled: 5
}
aasm column: 'state', enum:true do
...
end
Lets take a look back to check what all did ‘AASM’ do for us
Set default value for our state field
Validate transitions from one state to another
Callbacks to invoke function on state transition
Instance methods to get/set state with other useful helpers
Scope methods to query db for required states
Imagine having to do all these without using state machine.
There are many other things under AASM that we can customize and extend it as required. Refer gem documentation for more details.
So that’s a taste of state machine. Hope you’re taking away something good from this post.
Cheers!