Designing solutions with state machines in Elixir
Tags: elixir, statemachine, functional, programming
Published: 2021-11-07 02:27:23

Have you ever needed to build solutions related to execution flows like wizards, admission processes, or game rules in your software? If you do, you know it can be a nightmare of conditionals and complexity. We can make our lives easier by developing them using [state machines](https://en.wikipedia.org/wiki/Finite-state_machine).

In this post, we will see how easy the implementation of such a thing is in Elixir by using the [pattern matching](https://elixir-lang.org/getting-started/pattern-matching.html) feature.

## The basics

With a state machine, you can declare a set of states and transition controls to ensure the correct execution of a given flow. States represent data at a specific moment in time, and transition controls coordinate the change of states in a system.

Let's use a website's user registration process as an example. Take a look at the diagram below:

<figure>
  <img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/997uk3m4pqvth5mscqu2.png" alt="state-diagram">
  <figcaption>
    <p>
      The system will send an email confirmation after the submission of the form. We can also resend it if something unexpected happens.
    </p>
  </figcaption>
</figure>

We will use three states (`registration_form`, `awaiting_email_confirmation`,
`registration_finished`) and three events (`form_submitted`, `resend_email_confirmation`, `email_confirmed`)
to meet our requirements. The transition control will guarantee that we will go to the correct state every
time an event fires.

Now, let's understand how to do that in practice.

## The implementation

Our code starts with the definition of a new struct called `User`. The attribute that matters for our example is the `state`,
with the initial value of `registration_form`.

```elixir
defmodule User do
  defstruct [:name, :email, state: :registration_form]
end
```

We can check if everything is working by opening an iex session with our code:

```elixir
iex> %User{}
%User{
  email: nil,
  name: nil,
  state: :registration_form
}
```

Perfect. Our `User` module also needs a function that changes the state from one value to another - the
transition control. Its signature will be `User.transit(%User{}, event)`.

```elixir
defmodule User do
  defstruct [:name, :email, :password, state: :registration_form]

  def transit(user, event) do
    {:ok, user}
  end
end
```

Remember that our transition controls exist to ensure our states follow the correct flow.
According to our diagram, the first change is from `registration_form` to `awaiting_email_confirmation`, and it translates to the following in our code:

```elixir
defmodule User do
  defstruct [:name, :email, :password, state: :registration_form]

  def transit(%User{state: :registration_form, event: "form_submitted") do
    {:ok, %User{user | state: :awaiting_email_confirmation}
  end
end
```

We pass the user struct and the event reporting what action happened, and it returns an updated struct with
the next stage of our flow. The transition is straightforward, thanks to the power of pattern matching.

The remaining transitions use the same idea:

```elixir
defmodule User do
  defstruct [:name, :email, state: :registration_form]

  def transit(%User{state: :registration_form} = user, event: "form_submitted") do
    {:ok, %User{user | state: :awaiting_email_confirmation}}
  end

  def transit(%User{state: :awaiting_email_confirmation} = user, event: "resend_email_confirmation") do
    {:ok, user}
  end

  def transit(%User{state: :awaiting_email_confirmation} = user, event: "email_confirmed") do
    {:ok, %User{user | state: :registration_finished}}
  end
end
```

Great! Let's go back to our iex session to test our code. Remember to recompile it before the test.

```elixir
iex> user = %User{name: "Luke", email: "example@mail.com"}
%User{name: "Luke", email: "example@mail.com", state: :registration_form}

iex> {:ok, user} = User.transit(user, event: "form_submitted")
{:ok, %User{name: "Luke", email: "example@mail.com", state: :awaiting_email_confirmation}}

iex> {:ok, user} = User.transit(user, event: "resend_email_confirmation")
{:ok, %User{name: "Luke", email: "example@mail.com", state: :awaiting_email_confirmation}}

iex> {:ok, user} = User.transit(user, event: "email_confirmed")
{:ok, %User{name: "Luke", email: "example@mail.com", state: :registration_finished}}
```

There's one thing left in our code. Let's see what happens if we try an unknown transition.

```elixir
iex> User.transit(%User{}, event: "email_confirmed")
** (FunctionClauseError) no function clause matching in User.transit/2    

    The following arguments were given to User.transit/2:

        # 1
        %User{email: nil, name: nil state: :registration_form}

        # 2
        [event: "email_confirmed"]
```

That happens because no transitions allow a change from `registration_form` with the `email_confirmed` event. We need a *catch-all* function to prevent errors like that.

```elixir
defmodule User do
  # ...
  def transit(_, _), do: {:error, :transition_not_allowed}
end
```

When we call `User.transit/2`, Elixir will check each transition in their definition order, and if none matches, the catch-all function is called. Note that we are ignoring the parameters because they don't matter. Let's try again:

```elixir
iex> User.transit(%User{}, event: "email_confirmed")
{:error, :transition_not_allowed}
```

Awesome! Now, let's compare our initial diagram with its implementation.

<figure>
  <img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/997uk3m4pqvth5mscqu2.png" alt="state-diagram">
</figure>

```elixir
defmodule User do
  defstruct [:name, :email, state: :registration_form]

  def transit(%User{state: :registration_form} = user, event: "form_submitted") do
    {:ok, %User{user | state: :awaiting_email_confirmation}}
  end

  def transit(%User{state: :awaiting_email_confirmation} = user, event: "resend_email_confirmation") do
    {:ok, user}
  end

  def transit(%User{state: :awaiting_email_confirmation} = user, event: "email_confirmed") do
    {:ok, %User{user | state: :registration_finished}}
  end

  def transit(_, _), do: {:error, :transition_not_allowed}
end
```

Simple, huh? We did all that in 17 lines of code without using a single conditional.

## State machine libs in Elixir

The Elixir ecosystem has some libs that abstract the logic of implementing the transition function to ourselves. One of them is the [machinist](https://github.com/norbajunior/machinist), which I wrote to help me manage state machines that don't require Ecto or processes. Also, non-developers can understand it thanks to its easy-to-read DSL.

Let's write our example using the lib:

```elixir
defmodule User do
  defstruct [:name, :email, state: :registration_form]

  use Machinist

  transitions do
    from :registration_form,           to: :awaiting_email_confirmation, event: "form_submitted"
    from :awaiting_email_confirmation, to: :awaiting_email_confirmation, event: "resend_email_confirmation"
    from :awaiting_email_confirmation, to: :registration_finished,       event: "email_confirmed"
  end
end
```

This code generates the exact implementation we did above, but it eliminates all the boilerplate, making it easier to read and maintain and less prone to errors.

You can explore some additional libs that also handle state machines if the machinist doesn't fit your needs:

- [machinery](https://github.com/joaomdmoura/machinery);
- [gen_statem](https://blog.appsignal.com/2020/07/14/building-state-machines-in-elixir-with-ecto.html) (actually an Erlang module);
- [fsmx](https://github.com/subvisual/fsmx#decoupling-logic-from-data).


## Conclusion

Designing solutions with state machines get our back, ensuring we won't have a corrupt state and securing our
software from unexpected behaviors. Beyond that, implementing it in Elixir helps us write intentional and
declarative code, allowing everyone to understand the big picture quickly.

I hope you learned something new today. Thanks for reading it!