gravity9 System Architecture Guide: Actor Model
02 Jul 2024 | Mateusz Kłosiński
Architecture is a vital aspect of all systems and applications, serving as a blueprint for engineers that defines behavior and structure. There are numerous architectural patterns out there and they can be used on different levels of a system – a single system can use multiple architectural patterns!
As a digital consultancy, gravity9 has a rich history and heritage of development, picking the best architecture for the job.
In this series of articles, we’ll introduce some of the most popular system architecture around. We’ll look at why they’re popular, where they’re useful, and where they’re less useful.
The Classic (Stateless) Approach.
Traditionally, when web applications require data to run a business process, a request to fetch it is sent to the persistence store (a database or cache). When the request is handled, the data is saved back to the store, and the application forgets it. Subsequent requests must repeat this process, retrieving the data again. The same information can be shared amongst multiple business processes (which can update it). The stateless approach can become challenging when requests are invoked by multiple clients simultaneously, and they process shared data.
The Concurrency Challenge.
Executing code in parallel creates an environment for errors that wouldn’t occur where code is executed in only one thread. Consider the following C# based example:
In a single-thread environment, you can invoke Count()
as often as desired, and the value
will never exceed 5
. In a multi-threaded environment, however, it could.
Imagine that the value
is currently 4
when two threads simultaneously check the if
statement and both increment the value, resulting in a value
set to 6
. The most common way to fix this issue is to introduce a lock
, as follows:
The value value
can now not exceed 5; however, the code can only be executed via a single thread. If used often, this solution can introduce unwanted performance bottlenecks and should be approached with caution.
Like code, databases can also present challenges with concurrency. Consider the same data below:
Imagine that two users simultaneously fetch this information from a front-end application, modify it, and save the change. The recorded result will be whichever user saved last, and the other user won’t see their change reflected. This behavior may not always be acceptable, and again, one solution is to use locks.
There are two suitable types of lock for this scenario: optimistic and pessimistic. The optimistic lock looks like this:
The table has a fourth column called version
. The value in this column should change each time the record is updated and sent along with the modified data to any requests. This allows us to check that the version being worked on is the latest version or outdated by a modification from another user or source.
These two simple examples of code and database concurrency issues show that working with concurrency is not always straightforward. As codebases, databases, etc., grow, issues of this nature become increasingly hard to spot and mitigate.
Introducing Actor Model!
The actor model was introduced to overcome concurrency challenges while maintaining a relatively clean codebase. In computer science, it is a mathematical model of concurrent computation that treats the actor as the basic building block of concurrent computation.
The actor has the following characteristics:
-
It communicates through asynchronous messages.
-
It contains an in-memory state.
-
It changes the state through behaviors.
-
A single thread processes its logic.
With an actor model, you can have as many actors as you want. Multiple actor’s code can be executed in parallel, but each actor can be processed by only one thread. This means that an actor’s code doesn’t need to be protected by locks, as was necessary in the concurrency examples covered earlier in this article.
An example of this in C# code looks like this:
The example operates the same counting logic as our earlier concurrency example but does so using the Proto.Actor
.NET framework.
The actor has its own:
-
Internal state (
_count
field). -
Handler method to process incoming messages.
-
A dedicated method to increment the counter when the message of type
IncrementMessage
is received by the framework.
This illustrates that the actor model can overcome challenges concurrency can cause while also avoiding unwanted performance issues caused by, e.g., locks in the code.
There are several implementations of the actor model and virtual actor model in .Net, with the most popular being Akka.NET, Orleans, and Proto.Actor, and Dapr.
Virtual Actor Model.
The Virtual Actor Model is an extension of the Actor Model. It moves the responsibility of the actor’s instance to the runtime (Virtual Actor Framework). There is an assumption that the machine that the actor is running on should be transparent.
To achieve this, there are some prerequisites:
-
The actor must be uniquely identified.
-
There must be persistent storage for the actor’s state.
With those fulfilled, the actor can be dynamically moved between different nodes. When one node fails, the framework can use the state to recreate the actor on another machine. As the location is transparent for the client, the solution can also be dynamically scaled.
Actor Model Advantages.
The actor model provides some obvious advantages, especially when dealing with concurrency-based situations:
-
Parallel Processing: The actor is processed by a single thread and is solely responsible for its in-memory state. Even when multiple actors are invoked in parallel, they won’t be prone to concurrency issues as they don’t access the data used by other actors (there is no shared state).
-
In-memory State: Processing of the actor’s logic does not require any I/O operations. It uses only its in-memory state, which is faster than fetching data from a database or even cache!
-
Scaling Efficiency: Actors only use asynchronous messages to communicate. When sending a message, you need to know the type of an actor and its identifier, but you do not need to know where the actor is. This means that you can have multiple nodes running simultaneously, and only the node that has the actor with the provided identifier will pick up and take care of relevant message processing, while others are unaffected.
Actor Model Disadvantages.
-
Cluster Topology Changes: When more computational power is needed in a stateless approach, adding or removing nodes is generally harmless, and they work like already operating instances. In the Virtual Actor Model, however, each node contains actors that have a state. When a new node is added, nothing is changed, as existing actors are already working on existing nodes. For optimum performance and to take advantage of new nodes, a strategy is required to move existing actors to newly created node(s), distributing load evenly. Those strategies vary between different virtual actor frameworks, so you must choose the one that best fits your needs.
-
Consider the Learning Curve: The Actor Model is a novel approach that is less widely practiced than the classic stateless approach, meaning that implementing it may create a skill gap in development teams. When deciding to use the Actor Model or Virtual Actor Model, consider the time developers need to get familiar with these approaches and their associated tools.
-
Testing and Debugging: The Actor Model was designed for concurrent computing. Adding the distributed nature of the Virtual Actor Model can create a more demanding situation regarding debugging and testing, which may even require additional tooling. It’s important to consider these additional demands on project teams when considering a Virtual Actor framework solution.
Is Actor Model right for YOU?
The stateless approach can present challenges and performance degradation in situations where concurrency is required, especially in increasingly complex systems featuring many entities. This is where the Actor Model and Virtual Actor Model step in to save the day, offering a great solution in scenarios where many small entities exist independently and with their own internal state. The actor models prove their worth when scaling is important, as their asynchronous communication lets them efficiently interact with only relevant nodes. This makes the actor model especially useful in social media applications (tracking user statuses, messages, and notifications), gaming (tracking players in online games), and Internet of Things (tracking multiple devices’ state changes).
However, the Actor Model is not a solution without trade-offs and consequences. Its increased complexity and deviation from traditional models make it less well-known among development and testing teams, creating a learning curve to factor into an implementation. Furthermore, when systems scale and create new nodes, specific strategies must be implemented to ensure the Virtual Actor Model continues to work effectively without presenting its own bottlenecks. In situations where processing is synchronous, requests must be processed in the order they’re received, or the application doesn’t have a mutable state – the actor model may not be the best choice.