Software projects often start with such innocence. A developer creates the basic functionality and then extends the functionality with various rules of domain logic. Project classes grow larger and more complex to accommodate all these changes. The classes gain a lot of different attributes, while the program logic itself becomes knotted up with a lot of ‘if/else’ conditions. Soon, it has become spaghetti code we all loathe.

And we loathe it for good reason. It is incredibly difficult to support such complex code, and it becomes more and more difficult to incorporate new features in response to demands of the business user base.

How can we avoid such complications? By using an architecture based on finite state machines right from the beginning of the software project development.

What is a finite state machine?

A finite state machine is actually a processing model built on the idea that object can exist in only one state any given time. A traffic light, for example, has only three states: green light, yellow light and red light. Moreover, it exists in only one of those states at any given point. It can’t be green, yellow and red at the same time (old school backend developers from Massachusetts may debate me on this , but everyone else will get the point).

By defining the different states and how they are both related to and constrained by one another, we can avoid the circuitous complications that arise from innumerable ‘if/else’ conditionals. The options are clearer and cleaner; extensions and enhancements are more readily slotted into the existing logical structure of the application.

Building finite state machines in Django

Designing the architecture of an application with finite state machines involves three simple steps:

Selecting a proper data model Defining valid states for such model Selecting transitions between these states

Let’s explore how we would do this using a readily understood sequence of actions — in this example, the steps associated with processing an order for something.

To start with, we need to define an Order model. In this case, that model will include at least two fields:

Product (which contains a text description of a product)

(which contains a text description of a product) Amount (which contains the product’s price)

Here’s what that model looks like in Django:

from django.db import models class Order(models.Model): amount = models.DecimalField(max_digits=9, decimal_places=2) product = models.CharField(max_length=200)

According to the rules of the application domain logic, each Order can have only one of the following states:

Created

Paid

Fulfilled

Cancelled

Returned

We’ll add an integer-value Status field to our Orders model and constrain it to the list of available states. In Django, that addition looks like this:

from django.db import models class Order(models.Model): STATUS_CREATED = 0 STATUS_PAID = 1 STATUS_FULFILLED = 2 STATUS_CANCELLED = 3 STATUS_RETURNED = 4 STATUS_CHOICES = ( (STATUS_CREATED, 'created'), (STATUS_PAID, 'paid'), (STATUS_FULFILLED, 'fulfilled'), (STATUS_CANCELLED, 'cancelled'), (STATUS_RETURNED, 'returned'), ) amount = models.DecimalField(max_digits=9, decimal_places=2) product = models.CharField(max_length=200) status = models.SmallIntegerField(choices=STATUS_CHOICES)

As an order is processed, it passes through a sequence of these states. An object is Created, Paid for, and finally Fulfilled. Any other sequence of status states would make no sense. An order can’t be Fulfilled, for example, unless it has already been Created and Paid for. Similarly, logic dictates that certain states should not be accessible unless an order is already in a specific state. An order cannot be Cancelled unless the Status state is already listed as Created or Paid. Likewise, a product cannot be Returned unless its current state is Fulfilled.

How do we establish this network of state transition rules and relationships? We can use the django-fsm library.

The Transition decorator in django-fsm can facilitate transitions between the available states associated with the Order object. Let me draw your attention to certain key parameters that are required for Transition to function properly:

The Status parameter in Order must identify a database field that holds the current state of the object.

parameter in must identify a database field that holds the current state of the object. The Target parameter of this decorator must indicate the desired Order state after using the Transition method.

parameter of this decorator must indicate the desired state after using the method. The Source parameter contains information about the possible initial state (or a list of such states), which allows transition to the Target state.

Now let’s create Transition methods for each of the available states in our order processing example. We will also set the default state for all new objects as Created. Note, too, that the Transition method requires that we redefine the Status field type as FSMIntegerField.

from django.db import models from django_fsm import transition, FSMIntegerField class Order(models.Model): STATUS_CREATED = 0 STATUS_PAID = 1 STATUS_FULFILLED = 2 STATUS_CANCELLED = 3 STATUS_RETURNED = 4 STATUS_CHOICES = ( (STATUS_CREATED, 'created'), (STATUS_PAID, 'paid'), (STATUS_FULFILLED, 'fulfilled'), (STATUS_CANCELLED, 'cancelled'), (STATUS_RETURNED, 'returned'), ) amount = models.DecimalField(max_digits=9, decimal_places=2) product = models.CharField(max_length=200) status = FSMIntegerField(choices=STATUS_CHOICES, default=STATUS_CREATED, protected=True) @transition(field=status, source=STATUS_CREATED, target=STATUS_PAID) def pay(self, amount): self.amount = amount print("Pay amount {} for the order".format(self.amount)) @transition(field=status, source=STATUS_PAID, target=STATUS_FULFILLED) def fulfill(self): print("Fulfill the order") @transition(field=status, source=[STATUS_CREATED, STATUS_PAID], target=STATUS_CANCELLED) def cancel(self): print("Cancel the order") @transition(field=status, source=STATUS_FULFILLED, target=STATUS_RETURNED) def _return(self): print("Return the order")

Once we have created the model, we can use the Transition decorator. Take a look:

# Let’s create a pizza order >>> order = Order() >>> order.product='Nice pizza' # Now we need to pay for it >>> order.pay(10) Pay amount 10 for the order # And deliver it >>> order.fulfill() Fulfill the order # We can’t cancel the order when it was fulfilled >>> order.cancel() Traceback (most recent call last): File "/usr/lib/python3.5/code.py", line 91, in runcode exec(code, self.locals) File "", line 1, in File "/home/dima/lib/python3.5/site-packages/django_fsm/__init__.py", line 515, in _change_state return fsm_meta.field.change_state(instance, func, *args, **kwargs) File "/home/dima/lib/python3.5/site-packages/django_fsm/__init__.py", line 299, in change_state object=instance, method=method) django_fsm.TransitionNotAllowed: Can't switch from state '2' using method 'cancel'

# We can only return it >>> order._return() Return the order # The returned order can’t be fulfilled again >>> order.fulfill() Traceback (most recent call last): File "/usr/lib/python3.5/code.py", line 91, in runcode exec(code, self.locals) File "", line 1, in File "/home/dima/lib/python3.5/site-packages/django_fsm/__init__.py", line 515, in _change_state return fsm_meta.field.change_state(instance, func, *args, **kwargs) File "/home/dima/lib/python3.5/site-packages/django_fsm/__init__.py", line 299, in change_state object=instance, method=method) django_fsm.TransitionNotAllowed: Can't switch from state '4' using method 'fulfill'

The finite state machine model provides a great way to solve the problem of project complexity. By using finite state machines, you can decrease the overall number of errors caused by inconsistency in the system. Furthermore, you can use finite state machines to bring order to the code, making it easier to expand your options in the future.

At Distillery we use finite state machines in a wide range of projects, and not just in order-processing scenarios. Finite state machines are quite useful in authorization modules, social networks, internet marketing, electronic payment systems, medical services, and many other use cases.



About the Author

During Dmitry Stepanenko’s long career he has gained skills as DBA, information security specialist, full-stack developer, and even a DevOps engineer. Dmitry is a huge fan of the Python programming language and is always ready to evangelize on its behalf and bring non-believers into the folds of enlightenment. He has been with Distillery since 2016.