June 26, 2017 – Adrien Di Pasquale – 4-minute read
State machines are a very powerful tool but are often underused in web development. The design process forces you to think hard about how you want to model your data, about the different objects lifecycles, about the way you want to expose your data and communicate with your whole team, and about the upcoming evolutions.
Going through this process takes a lot of efforts but is worthwile, it brings a lot of structure to your code and your team. Also, the actual implementation of a state machine is usually very simple.
A simplified state machine for a a
Movie object can be represented like this :
And this diagram was generated with the following Ruby code:
# first, gem install 'state_machine' class Movie state_machine :state, :initial => :in_production do event :finish_shooting do transition :in_production => :in_theaters end end end movie = Movie.new movie.state # in_production movie.finish_shooting! # will raise if something goes wrong movie.state # in_theaters # to generate the diagram : $ rake state_machine:draw CLASS=Movie FORMAT=svg
see the state_machine gem for more information
In the context of a fast evolving product and a growing team, the aimed qualities of a state machine should be:
There is no one-size-fits-all solution and a lot of questions will have many valid solutions. An infinity of state machines could represent your data, and you could make your app work with them. You need to pick the one that makes the most sense for your needs and your vision.
Here are some tips to help you make these decisions:
Designing a state machine should be a collaborative process. It is important that developers share their opinions and agree on a structure, so they will be willing to use it afterwards.
It is also extremely important to go talk to people with other roles in your team, to understand how they talk about the data and how they interact with it.
Here is a quick example to illustrate the diversity of viewpoints:
Unfortunately, when designing state machines it is often hard to reach an unanimous and universal truth. As pointed above, different teams opinions are all valid in their context. Also, as the product evolves, the truth evolves.
It is your responsibility to decide what to preserve from the different opinions and what you will go against. It is important that you have all the elements to decide, and make a conscious and reasoned choice, so you can justify it to other people.
Don’t rush, reaching consensus is a time-consuming process.
In the context of a startup like Drivy, the product roadmap and the strategic directions are likely to change often. Some decisions will reveal to have been short-sighted, sub-objects may appear, you may have to add extra transitions for edge-cases, etc…
It is useful to think about the degrees of freedom your design leaves open. You can orient and pick these degrees in the directions you think are more likely to happen.
When you do not feel extremely confident about the forecast evolutions, it is a good advice to try and make the least engaging choices. It often boils down to creating the least states possible, because it is easier to split them later than to merge them (from our experience at least, your mileage may vary).
Storing only the current state on objects is dangerous. Investigate objects in corrupt states is complicated, as you cannot understand how they ended up there. Also, when you have to make changes to your state machine, you have much less flexibility on how to migrate objects because you cannot distinguish them.
We strongly recommend you archive all the different states, transitions and events that objects go through. A versioning library like the papertrail gem can help in that matter.
This was initially presented as a talk at Paris.rb on 05/07/2016