By using our website, you agree to the use of our cookies.
By using our website, you agree to the use of our cookies.

Using the Actor Model in a .NET Microservice

When creating new services with .NET, developers often choose an n-tier model, usually with a data layer, logic layer, and an API layer. Various design patterns are used, such as the Service Pattern and the Repository Pattern, to shape the different layers. This is a proven concept that works well for many types of services. However, there are challenges:

 

To make the service scalable so that it can handle many requests, the entire application or service must be stateless or an external cache must be used which brings its own (invalidation) challenges. Stateless services place a greater burden on a database system because data must be retrieved for each request. In these solutions, you often see heavier and more expensive database systems to handle the number of requests. The scalable service is not the bottleneck, but the database system is, and these are often only vertically scalable. Horizontally scalable databases are on the rise, but they are usually very expensive (example: Azure Cosmos DB).

 

To make the service very performant, you need to use the multiple cores that processors have today. Taking into account multiple threads that can all run simultaneously makes it very complex. Often you see quick fixes such as the use of “Monitor.Enter” and “Monitor.Exit“, but this only blocks entire threads and usually does not improve performance at all.

 

Is there a model that can overcome these challenges and enable a modern way of developing? Yes, there is! And it has been around for a while, namely programming based on the actor model. The actor model is not new, but was invented as early as 1973. The fact that this model is now gaining popularity is due to all the multi-core processors on the market today and the idea that data must be processed directly (batch jobs, anyone?). A good explanation of the situation with traditional OOP and where the added value of the actor model is can be found here and here on the Akka site.

 

The .NET ecosystem has several actor model frameworks, but one of the most popular is Akka .NET. This framework is a direct port of the popular Akka for the Java and Scala programming languages. Apart from this implementation, a standalone framework has been built for .NET based on the same principles as the Akka variant, but it has its own roadmap and therefore its own direction. The Akka .NET framework is fully open source, although there is a commercial company (Petabridge) behind it where you can purchase support contracts, but this is not mandatory!

 

To get started with Akka .NET, there are several books available, but the best way to start is to go through the documentation on the site and the Bootcamp exercises. In short, the actor model is a system where Actors are objects with their own lifecycle (start, stop, restart) that are active within an “Actor System”. These actors all have a “Mailbox” where messages can be sent to them. The mailbox is a queue and the actor processes the messages in this queue. The messages are processed one-by-one, so there can never be multiple threads working within an actor. When an actor has processed a message, it can also send a message back to the sender (Sender). All of these actors and the message traffic between them are executed by different threads managed by the Actor System. So it is possible for Actor A to send a message to Actor B on Thread 1, but for Actor B to process the message on Thread 2.

 

Why would you want to use an actor model in a microservice? Because it is complementary based on concepts. You use a microservice architecture to separate domains and allow them to have their own development cycle that is separate from other domains. Each microservice has its own lifecycle and responds to messages (APIs) and events (via an event messaging system such as Event Hubs or Event Grid). Within the microservice, there are smaller sub-domains (the actors) that also have a lifecycle and receive messages or events (the Akka Event Bus).

 

If there is a stateless actor and it receives more messages in its mailbox than it can process, you can use a Router. A router distributes incoming messages to multiple actors of the same type. This way, you can make the functionality in these actors horizontally scalable. This is similar to using a load balancer in a microservice environment to distribute traffic across multiple microservices of the same type.

 

Another reason for using the actor model in a microservice is that it is a lightweight system. Actors are small objects that do not take up much memory, and the whole system is very efficient in its use of CPU resources. Often, you can use actors for small tasks and then stop them, freeing up memory again.

 

An interesting solution is the use of state within actors. In this approach, data is kept in the memory of the running service and snapshots are written to the database. With subsequent requests, the state does not have to be retrieved from the database, but is immediately available in memory. The immediate benefits are:

  • Requests can be handled very quickly because there is no need to make a request to the database, which can save tens of milliseconds. Especially in situations where fast responses are expected, it is important to eliminate all delays.
  • If the database is too busy, this has no immediate impact on the customer. The load on the database system is also much lower, even with a high number of requests. This means that a highly specced (and expensive) database system is not necessary.
  • It is recommended to create actors per entity (so per person, order, payment, etc.), which ensures that each entity has its own memory space and data from different entities cannot be intertwined if set up correctly.

 

Because the state runs in the memory of the running microservice, memory usage can increase as more actors with state are added. Therefore, it is always important to ensure that actors are stopped and state is cleaned up when the entity is no longer active. Akka .NET provides tools for managing this properly. But if the number of active entities running within a service becomes very large, there is also the option to horizontally scale the services and distribute all entities across instances of the same service. For this, Akka .NET has the ‘Clustering’ component where multiple instances can be connected. In practice, this can be combined very well with smaller microservices, so that not too much functionality is scaled unnecessarily, in a Kubernetes cluster with auto-scaling capabilities.

 

As you have read, the actor model is a very interesting option for developing microservices. The operation of the model is different from the traditional OOP operation of a system, and therefore requires a short learning process on how to make the best use of this model. If you are curious, please contact us. We use Akka .NET as our standard for building customer-facing functionality and have templates and examples available so that every developer can immediately start building services using this approach on our Avanti platform.

Find more interesting blogs here