State machines are often used at bitcrowd to model certain workflow processes. For instance, a post can at first be in a draft state, then once it is reviewed it transitions to a reviewed state. Finally after it has been published it’s in the published state. Modeling this explicitly with a state machine visualizes the process and the accompanying events and transitions instead of hiding it among other code. Of course, this is a simplified example.
So let’s say state machines are awesome and we want to use them - which gem do we use? There are a ton of state machine gems.
Before we continue, a little disclaimer: All of this is based upon the best of our knowledge resulting from an afternoon of looking at different state machine gems testing them out as well as longer project experience working with state_machine and acts_as_state_machine.
The most popular, and from our experience arguably best gem, is state_machine. However, sadly the gem is unmaintained - the last commit dates back to the 7th of May 2013 and slowly but surely the gem becomes incompatible with new releases of Rails/ActiveRecord. There are a ton of open pull requests to fix this, but so far no official new fork/version has emerged. Developers are left to pick and choose forks and manually patch things up. As work on Rails 5 already start this could be a uncertain and dangerous route.
The next most popular option is acts_as_state_machine, which is actively maintained and seems to be the current go-to solution. It is up to date and works in most cases. However, in our experience we found it somewhat clunky to work with. There were bugs and odd unexpected behavior along our way.
Another interesting option that we found is statesman. It features a differing but good approach compared to other gems. In statesman the state machine is a separate class which you then then instantiate in the model - composition rocks! Furthermore it keeps track of all state transitions, which can come in handy. When you want to persist them you need a separate model. Moreover, they do not define as many methods magically which might prevent unexpected behavior and leads to less “pollution”. It also seems to be well maintained. As a first impression it results in more lines of code and objects, but those are separated better and have their own responsibilities.
What we eventually discovered is that we used state machines in a way in which the gems don’t want to be used. We defined a set of requirements that we wanted a state machine gem to fulfill (additionally to their basic functionality):
- it should be possible not to persist an object after a state transition
- it should be possible to automatically advance states (recursively), if possible
- it should be possible to wrap a transaction around an invocation of a state machine (which might entail multiple state transitions)
We set up a test project to test if that behavior was achievable with a given gem. Spoiler alert: it is not. For some modeling an automatically advancing workflow isn’t even possible and most others rely on the fact that every state transition is persisted.
So what are the possible options?
- Grab a couple of state_machine forks, mix them together and get a working version
- Use acts_as_state_machine or another gem and let every state transition persist. A work around here is to pull the code of auto_transitioning_states into a ServiceObject that encapsulates the necessary tasks and their conditions and only triggers a transition to a next state once all steps are completed and the object should be persisted.
- Use statesman and take care of persistence yourself (using the memory adapter)
- Roll your own state machine gem, which of course is more awesome than everything else, although you greatly underestimate the effort it takes to write :)
Some note worthy mentions about gems to wrap up:
- workflow seems like a nice gem, however persisting new states bypasses all ActiveRecord callbacks. Also, it can’t model the recursive workflow.
- We really liked transitions at first, modeling our workflow needed some workarounds and sadly some ActiveRecord related callbacks are missing
- micromachine seems like a nice minimal state machine implementation without any adapter for databases or whatever so you can handle it yourself or base your own sate machine implementation upon it