Utilizing the Finite State Machine

How using a State Machine saved our apps & flows from refactoring

photo of Ahmed Shehata
Ahmed Shehata

Software Engineer

Posted on Jul 05, 2018

How using a State Machine saved our apps & flows from refactoring

There is a lot to learn about a "Finite State Machine" (FSM).

A little intro: what is a FSM?

A Finite State Machine is an abstract model of computation, which can be in only one finite state at a specific moment. Finite State Machines are used to model problems in different domains such as AI, games, application flows, etc.

In simpler words: It describes how a program should behave by specifying pre-specified states and routes between them.

A Real World Example

Let's imagine a safe lock:

null

Simply, this lock has two states: locked and open. Depending on the transitions between these states, below diagram shows the routes/transitions.

null

Let's say every action is a transition, so every button you click on the lock, it will still be in the same state: button pressed.

Only after entering the correct combination, will the lock move to the open state. Afterwards, there is a security timeout that returns to the locked state after a certain time has expired.

Let's imagine a very simple manual way to code this lock in Javascript:

const OPEN_STATE = "open";
const LOCKED_STATE = "locked";
const lockTimeout = 3000;

class StateMachine {

  constructor(code){
    this.state = LOCKED_STATE;
    this.code = code;
    this.entry = "";
  }


  enterDigit(digit) {
    this.entry += digit;
  }

  unlockDevice() {
    if(this.entry === this.code) {
      this.state = OPEN_STATE;
      setTimeout(this.lockDevice,lockTimeout);
    }
  }

  lockDevice() {
        this.state = LOCKED_STATE;
        this.entry = "";
  }

}

const fsm = new StateMachine("123");
console.log(fsm.state);

fsm.enterDigit("1");
fsm.unlockDevice();
console.log(fsm.state); // prints "locked"

fsm.enterDigit("2");
fsm.unlockDevice();
console.log(fsm.state); // still "locked"

fsm.enterDigit("3");
fsm.unlockDevice();
console.log(fsm.state); // "unlocked"

Every time unlockDevice() is called, it checks if the current entry matches the code. This is called the transition condition. If true, it allows the state to transition to the next (or previous state).

Here are some examples of FSM libraries in Javascript that you might find useful:

Our use case

At Zalando, our team is responsible for building the Guest Checkout Flow to allow non-Zalando customers to be able to purchase without an account. We first started with the basic flow and didn't have much in mind on what was to come.

The basic flow was:

Product Page -> Personal Info -> Address Info -> Payment -> Confirmation -> Receipt

Every page in this design was responsible for the transition to the next page, example:

// product-detail.js

// ...
const buyButtonClicked() => {
    goToPersonalPage();
}
// ...

// personal.js

// ...
const confirmButtonClicked(personalInfo) => {
    if (personalInfoComplete(personalInfo)) {
        goToAddressPage();
    }
}
// ...

But there's one small flaw with this simple design. It's not extendable, not even testable.

Our product team wanted to introduce some new functionality to the flow, namely "Login Functionality," which would completely break the whole design.

Logged in users, without personal info or Address info:

Product Page -> Login -> Personal Info -> Address Info -> Payment -> Confirmation -> Receipt

Logged in users, without payment info:

Product Page -> Login -> Payment -> Confirmation -> Receipt

Logged in users, without address info, BUT HAVE PAYMENT:

Product Page -> Login -> Address -> Confirmation -> Receipt

Logged in users, without payment info:

Product Page -> Login -> Address Info -> Payment -> Confirmation -> Receipt

And what about Guest Users now? Too much if-else.

Enter The State Machine

This design screams for a state-machine like design. We laid down the states we want, defined some rules between them, and let the state machine do it's magic.

null

This is a simplified example of how the FSM would work. If you notice, almost all pages return back to the FSM for consultancy on where to go next. The FSM has validation rules that allows it to decide what to do next; it uses the Redux Store to decide.

We called this function, goNext(). We defined all the possible rules and transitions we have in the system; a fallback would be to just render the product page if the state is not compatible with any of the transitions.

The state machine takes the state, follows through the rules and keeps "going next" until it finally reaches the proper state.

An earlier example of a user with personal + address but with no payment would be:

*Personal state: User? Has personal? Yes? Go next. Address State: Has address? Yes? Go next. Payment: Has payment? No? Stay here. *

A challenge to that design

A good challenge to this design was the implementation of “going back.” The state machine was design to always move forward, right? What happens if the user decides to go back to the previous page? Luckily the Redux State System manages this, however, it was not implemented in our initial design with goNext(). The answer is simple. We implemented goPrev(), which would have the same concept as going forward, just the other way around. Same rules apply: different direction. It worked quite well (after ironing out some nasty bugs).

Pros of this FSM Design

  • Easily maintainable, transitions and states are clearly defined
  • Testable, unit tests can easily be written with pre-defined states for multiple scenarios
  • Easily extendable, allowing for new states to be just plugged in along with their rules

Cons of this FSM Design

If some scenarios are not well defined, the FSM just redirects the user to the product page when they were almost in the payment page. For example, if some underlying backend service (e.g., a payment provider) returns an unexpected response, the Redux state would get corrupted and the FSM wouldn't know what to do, redirecting the user to the product page, leaving the user confused with, "What on Earth happened to my credit card now?"

We try to cover as many scenarios as possible, also providing the user with a proper error page so that they do not get confused.

A next-step improvement would be allowing the FSM to "re-try" if something fails.

And as they say, computers and humans aren't perfect.

Follow more of Ahmed's writing here, or have a look at our open tech positions to work with people like him!



Related posts