Finite State machine - when to use and when not to?

What are use cases when Finite state machines should not be used?

  • Must not use because ______

  1. MAY USE or MAY NOT use
  • Pro of using FSM are : (a) ____ (b) ______ (c) _____
  • Cons of using FSM are : (a) ____ (b) ______ (c) _____

  1. MUST USE in THESE cases
  • because ____

This is a general discussion to gauge where are FSM’s useful. I am new to this concept and trying to understand if I should refractor some of existing code to use FSM’s.

1 Like

You’ll get significantly better responses, IMO, if you ask about your specific use-case versus sifting through mountains of speculation. That said, here’s a small pile of speculation…

Must not use:

  • things where the space of states is infinite. You wouldn’t use an FSM for “current account balance”

Pros of using:

  • FSMs provide a shared vocabulary for talking about complicated code. Snazzier libraries will even let you run the documentation!

Cons of using:

  • using an FSM with the wrong “kind” of implementation can be painful. For instance, using something compile-time like :gen_statem or Finitomata (linked above) but with states/transitions defined at runtime
  • an FSM is good at enforcing business rules in software like “do X then Y”, but that can cause friction at a company where the real-world process is “do X then Y, unless it’s Tuesday then do Z first instead and then Y and then X”

“Must use”:

  • It depends on what you define as “an FSM”: is a “draft” boolean that starts out false and is deliberately set to true later an FSM? It has two states and a transition between them. :thinking:

Thanks for advice. Will keep in mind.

Thank you for explanation. The above blog post mentioned in link, was an inspiration. (Even though I don’t understand it fully yet).

Not necessarily. Here is an example with finitomata pseudocode.

x --> |from_x| y
x --> |from_x| z
z --> |from_z| y
y --> |from_y| x

and then

def on_transition(:x, :from_x, event_payload, state) do
  if event_payload.thursday?, do: {:ok, :z, state}, else: {:ok, :y, state}

I think any time you have to model data with 3 more more exclusive states, you are writing a FSM. So I take this question to essentially be one of implementation, and specifically whether to use libraries or not.

I used FSM libraries heavily in Rails, the most recent legacy app I rewrote was just using timestamps to determine states which was very messy, difficult to reason about, and bug prone. Converting to proper/explicit state machines was a no brainer for me. I think there was briefly a trend against FSMs for whatever reason, I remember reading this article at some point. It skims the Rails history topic of observers which essentially was part of the perennial struggle with the type of callback hell that could affect Rails apps. But again, I don’t think there is any avoiding state machines in most apps of any significant complexity.

I feel like I end up writing a version of this on every other thread I reply on, but one of the things I deeply appreciate Phoenix and Ecto’s design in particular is that, in forcing you to be a bit more explicit in how you handle this kind of logic, and arming you with the context/schema separation off the bat, these kinds of issues I faced constantly as a Rails dev are much weaker threats. So far I have just been using context functions with pattern matching to control transition logic, as @sasajuric advises in the disclaimer of the top of his fsm library, which is I found when I first went looking for a way to handle this logic in Phoenix apps. So far that has been working well for me, although of course as others have mentioned above it can get a bit sticky when there is a lot of conditional logic around transitions. But in my experience libraries can only help so much with that kind of complexity since it is unique to each application. Basically I find the situation comparable to the factory pattern, where the small benefit in terms of reduced boilerplate and enforced consistency that you get from a 3rd party lib is not worth the simplicity of just sticking with POCFs (plain old context functions).