Today I am going to discuss two popular programming abstractions: actor model and state machine. I'll explain how and where they can be used in software development and show you why it might be a good idea to incorporate them into your toolbox.
Let's start with a real-life story that got me thinking about actor models and state machines. Once, I wanted to book a flight online, and the implementation almost drove me crazy. I opened the website, searched for flights, filled in passengers' details, added a few extras, submitted several forms, etc. Business as usual this far.
Then it asked for passport numbers, dates of birth, addresses which I don't remember by heart. So I left the PC, fetched the documents, spent another five minutes filling everything in, and was finally ready to proceed. I clicked the button, and immediately the window popped up saying: "Your session has expired. Please start over."
Wow? I just spent almost 30 minutes filling all required forms, and all I get is a timeout? That's far from a pleasant user experience. An ordinary user would, at this point, take a couple of deep breaths and retry, but I started thinking why it happened. Quick inquiries appeared in my mind:
These are the cases where the actor model and the state machine will help create a more dynamic, scalable, and friendlier user workflow.
The state machine is a popular programming paradigm that allows to model and describe states and transitions. Sometimes it's referred to as a finite-state machine as it has a finite number of states. Rule number one is that a user (or a program execution) can be in one state at any time, not in multiple. It's usually also specified how you can transfer between states. For example, it can be allowed to switch from state A to state B, but not to state C. There might be actions or triggers that initiate the state transition. A real-life example is a light switch. It has two states: on and off, and you switch between them by pressing a button.
You can find many implementations of this principle in different programming languages – .Net developers will find these State Machine workflows helpful provided with the framework or [dotnet-state-machine/stateless] library. You might also use alternatives or implement a custom state machine for your project.
Let's continue with a definition of an actor model. It's a mathematical model and programming paradigm from the 1970s, but it's trendy even these days. Mainly because of the way it can simplify concurrent programming. The actor is a basic computation unit that contains data and logic. Data is usually referred to as an actor state but don't confuse it with the states from above. Actor logic can only be triggered through a message. Those messages can be processed by actors asynchronously, one at a time, which guarantees actor state consistency. Nothing can modify the actor state except a message. Actors can be identified by IDs, the user ID, for example. The underlying implementation handles the creation of the actor and other lifecycle activities. A clear benefit is that a programmer doesn't need to check if the actor exists or not. All you need is an actor of a specific state with a specific ID to which you can send particular data and asynchronously receive results.
Practically it might look like this – get an object by an ID, invoke a method by passing particular data, start the desired action when method invocation is finished, and the result is ready. This is nothing new if you are familiar with Object-Oriented Programming.
Multiple languages (i.e., Erlang) support actors out of the box when it comes to implementation. Others have libraries and frameworks that incorporate the actor model – AKKA (and AKKA.Net), Orleans, or DAPR, to name a few.
Orleans, for example, runs in clusters, scales horizontally, supports multiple persistence options and provides easy ways for actors' deactivation. What started as an internal research project at Microsoft turned out into a public open-source. One of the early use-cases was back-end support for online games such as Halo, with millions of users worldwide. So why is Orleans useful?
What is excellent about Orleans's performance is that actors reside in its cluster nodes and can be deactivated after some time. Actor state such as user data can then be saved into storage or database.
Why does this matter? When a player leaves a game, there is no need to keep his data in memory. However, the same player may rejoin the next day, and that's when their actor becomes activated and updated with the latest data.
DAPR uses the actors model as well. But it does more than that. It is a platform for building distributed apps. DAPR is a more recent and also open-source project from Microsoft, and it utilizes a concept of a sidecar application that abstracts the complexity of distributed systems development.
Let's say I want to publish a message from a service written in C#. The message is to be consumed by other services written in other languages. Thanks to DAPR API, I don't have to worry about the message bus and its client's API because DAPR handles everything.
The persistence of the actor's state is abstracted as well. There is no need to worry about specific database access providers. It is done via cluster configuration. Whether data resides in Redis, relational database such as SQL Server, or somewhere else, it doesn't have to be part of the application business logic.
Enough of theory. Let's discuss how we can smoothen a car rental reservation with the tools described above. Renting a car is always a semi-manual procedure that involves multiple stages, and there is undoubtedly room for automation. How? What can a developer do to speed up the car renting process?
Let's imagine a perfect booking process that can be achieved by implementing a few back-end upgrades. You open the car rental website, choose dates and browse through the cars catalog. Many rentals fail at this stage because they don't have an availability calendar to show what cars are vacant. But it's not the only problem.
When you sign up, you should receive an ID. It could be an email, phone number, or preferably a system-generated number. If you start the booking process, the app creates an actor instance filled with initial data. When you choose a car, the car should have its ID and also be in the actor state. Like this, you'll ensure there is no data loss if the booking is interrupted. Then you can move to the next phase. The website will let you select extras like a child seat, GPS, or additional insurance. You tick some of these options and proceed. The underlying actor has a state machine that triggers the state change, e.g., from car selection to rental extras.
The current phase is always stored in the actor state, so you won't lose your progress if your connection drops. You can even switch your devices and still continue where you left. Of course, developers can implement additional triggers, e.g., you want extra insurance --> we'll need this additional information. So what happens when everything is filled and ready to submit?
A waiting phase is handy. You submit data, and the state machine switches to the "waiting for approval" phase and notifies the back-office system. Car rental employees can immediately review the data and documents which they get from the actor state. When they approve them, they just send a message back to an actor to proceed to the next phase. Then the actor transitions to the "booking confirmed" stage and notifies the customer. The front-end app can fetch all the data from the actor state at any time. Of course, there may be other states like "booking canceled" or "booking rejected".
I believe this workflow sounds way friendlier than the one I encountered when I booked a flight. The state machine assures that the phase of the process is always defined. Therefore there is no need to start from the beginning if interrupted or changed to another device. The process can also become more dynamic with transitioning logic based on user input.
Workflow modeled and programmed this way will surely boost the user experience. The current phase of the process is always defined. There is no need to start from the beginning if interrupted or when switching to another device. The process can be pretty dynamic with transitioning logic based on user input. Also, the development of this process might be less complex as there is less need to manage concurrent data access.
Is it all rainbow and sunshine, or are there any drawbacks? Sure, there are. Each technology, model, and implementation require some effort to learn its principles and API. Debugging the actor state might also present a significant challenge. Also, if the app runs in a single cluster, there is no need to worry about data consistency. But if you use a multi-cluster environment and blue/green deployments, it's a whole other story.
Reach out to get more information.
Oleg, sr. software engineer
If you develop games in Unity, this article could bring a good value: https://www.toptal.com/unity-unity3d/unity-ai-development-an-xnode-based-graphical-fsm-tutorial