State Machines in SQL
State machines are a powerful tool for modeling complex systems. They can be used to model everything from simple workflows to complex business processes. In this post, we’ll explore how to implement a state machine in SQL.
The basic idea behind a state machine is that an object can exist in one of a finite number of states, and transitions between states are triggered by events. For example, a simple state machine might model the lifecycle of a blog post, with states like “draft”, “published”, and “archived”. Transitions between states might be triggered by events like “publish” or “archive”.
The naive approach to implementing a state machine in SQL is to use a column in a table to represent the current state of an object. For example, you might have a state
column in a blog_post
table that stores the current state of a blog post. You could then write queries that update the state
column to transition the blog post between states.
However, this approach has a number of drawbacks. For one, it’s difficult to enforce constraints on the state transitions. For example, you might want to prevent a blog post from being published if it’s in the “archived” state. With the naive approach, you would have to write application-level code to enforce these constraints, which can be error-prone and difficult to maintain. A limited number of states and transitions can be enforced with a CHECK
constraint or through using enums, but this quickly becomes unwieldy as the state machine grows in complexity. States cannot be updated on the fly, and you would have to update the application code to add new states or transitions.
Furthermore, keeping a history of state transitions is difficult with the naive approach. You might want to be able to answer questions like “when was this blog post published?” or “how many times has this blog post been archived?“. With the naive approach, you would have to write additional application-level code to keep track of this information. Please, no database triggers.
A better approach is to model the state machine explicitly in the database. You can do this by creating a separate table to represent the states and transitions of the state machine. For example, you might have a state
table that stores the possible states of a blog post, and a transition
table that stores the possible transitions between states.
You may be asking why we keep the from_state
and to_state
columns in the state_changes
table while we already have the transition_id
column. I’ve found that it’s useful to provide an escape hatch here for overriding the transition logic. For example, you might want to allow an admin user to manually transition a blog post between states, even if the transition is not allowed by the state machine. The transition_id
column is therefore for contextualising the transition, especially in cases where multiple transitions are possible between two states.
Most commonly, you’ll want to get the current state of an object from the database. You can do this by selecting the most recent state change for the object from the state_changes
table using PostgreSQL’s DISTINCT ON
clause.
This operation is so common that you might want to encapsulate it in a view or a stored procedure. This means you can easily join the state_changes
table with other tables to get the current state of an object.
This returns the current state of each blog post, along with the event that triggered the transition and the name of the state. You can use this information to build powerful queries that answer questions about the state of your objects while retaining full context of the transitions. States can be updated on the fly by adding new rows to the state
and transition
tables, and multiple state machines can be modelled by adding a machine_id
column to the state
table. This allows you to model complex systems with multiple state machines which are configurable by the user, without having to change the application code.