Designing state machines

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.

Intro : State machines are 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

Design objectives

In the context of a fast evolving product and a growing team, the aimed qualities of a state machine should be:

  • simple : so it’s easy to understand and feels natural for everyone, not only developers.
  • useful : it should help developers build and maintain the app, not be an obstacle
  • adaptable : it should be thought out to be evolutive

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:

Tip 1 : Talk with everyone

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:

a movie seen by the Netflix Supply Team
a movie seen by the Netflix Marketing Team

Tip 2 : Accept that some choices are partial

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.

Tip 3 : Do not over-anticipate the future

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).

Tip 4 : Store everything

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

Related articles:

View openings 👍  Like this post? Join Drivy's engineering team!