Configuration-driven state machines
by Stephen Richards (Software Development Profession)
What do I mean by a state machine?
Many projects need a state machine to control workflows, You can read all sorts of computer science articles on what a state machine is, but the essence is that a particular object can exist in a certain number of states only, and events will cause a transition from one state to another. So think of a car, starting in “parked” state. In that state, it has just one possible event: “start motor”, and that will cause it to transition to “motor running” state. In “motor running” state, there are two possible events, “select first gear”, and “select reverse gear”, and so on. State machines are great for abstracting the possible states, events and transitions out of the model and into a separate module or object.
How we used a state machine
We are working on the Correspondence Tool project (https://github.com/ministryofjustice/correspondence_tool_staff), an internal tool for MOJ staff to control incoming questions, farming it out to various departments to answer, then forwarding for review and authorisation before finally responding. This is a typical and ideal job for a state machine to control the workflow.
I’m a great fan of using code that someone else has written and tested, so when it came to implementing the state machine, we turned to Statesman, one of several Ruby gems that provide state machine functionality.
Problems with the ruby gem approach
But as time went on, our user requirements became more complex. As we iterated on the tool, we found we needed several workflows (depending on the sensitivity of the question, and the level of scrutiny before the answer went out) for one particular correspondence type (Freedom of Information requests), and the future plans were to on-board several other types of documents, each with their own complexities. All the state machine rules were written in ruby code, and user-requested changes in the workflow became difficult to implement, harder to test and debug.
Solution: a configuration driven state machine
We decided to write our own state machine which would be configuration-driven. This has proven to be a better approach when you have great complexity. The configuration is specified in a YAML file, and you can see at a glance what events a particular user type is permitted to trigger on a particular document type in a certain state.
This is a snippet of the configuration file for Freedom of Information requests. This particular workflow is called “trigger”; it shows the the events available to users with the role “manager” in the “unassigned” and “drafting states”
Here we can see clearly what events are permitted to a user with the manager role on a case in unassigned state. Triggering the assign_responder event will cause the case to transition to the awaiting_responder state, and the request_further_clearance event is only available if the specified code snippet returns true. This has radically simplified the challenges of debugging and implementing changes to user requirements.
In the code, the actual events are triggered by calling the event with a bang on the state machine, for example:
kase.state_machine.assign_to_new_team!(……)
These method calls to the state machine are intercepted by a method_missing method — so no need to define a method on the state machine for each event — and expect to be called with a defined set of parameters. The state machine will then:
- Evaluate the if statement if present, and only proceed if the result is true
- Change the state on the case (if the transition_to statement is present)
- Fire off any after_transition hooks
- Switch to a different workflow if a switch_workflow statement is present
- Write a record to the transition history table for the case
Summary
- Abstraction of complexity: We found that the ruby-gem approach was great when we were dealing with a simple four or five step process, with just one workflow. But as soon as we had nine or ten steps, multiple user roles, multiple workflows, loops in the workflows, mid-stream workflow switches, the complexity became very hard to handle with ruby code.
- The configuration approach gave us much greater clarity over what the steps were, who was allowed to do what and when.
- Making changes to the workflows became much easier.
- Expect the configuration file to have a lot of repetition. Often one user type will have a very similar configuration to another for some states, but repetition here is the price you pay for clarity, and in our opinion, worth it.
- We soon found that one big config file was too big to handle, so now we have one file for each document type/workflow combination, and the state machine will concatenate them on start-up.
~~~~~~~~~~~~~~~~~~~~
If you enjoyed this article, please feel free to hit the👏 clap button and leave a response below. You also can follow us on Twitter, read our other blog or check us out on LinkedIn.
If you’d like to come and work with us, please check current vacancies on our job board (filter on Organisation::Ministry of Justice, Job Role::Digital)!
~~~~~~~~~~~~~~~~~~~~