JOIN US

PolarCape
May 14, 2024

Unveiling the Marvels of Akka.Net: A Symphony of Scalability and Concurrency in Software Development (Part 1) 

Akka.Net

“Unveiling the Marvels of Akka.Net” is a two-part post, co-written by two very special ladies! Our ‘two .Net Anas’ will be talking about a subject they work with closely, and which they have discussed in one of our .Net internal forums.

Ana Jordanova is a Senior .Net Developer with 7+ years of experience working in all stages of software development, including requirements specifications, architecture design, time estimations, development, and delivery. She has extensive experience gained through working in multiple domains, including investment portfolio management, election management, and health tech.

Ana Kostoska Gjurcheska is a Senior .Net Developer with 7 years of professional work experience in the IT field. She has been involved in most aspects of the software delivery cycle including requirement analysis, solution design, development, refactoring, making releases to environments and technical support. Her experience has been mostly in health tech, fin tech, telecoms, and hospitality.

Read their thoughts on Akka.Net below!

(written by Ana Jordanova and Ana Kostoska Gjurcheska)

Introduction

As systems are become more complex, concurrent, and fast in the ever-expanding universe of software development, let’s explore the actor model – the champion of distributed computing.  

This blog post series takes us into the world of Akka.Net, a framework where software components transform into actors on a virtual stage. In the digital theatre Akka.Net plays the role of the director, orchestrating a performance where each actor represents a self-contained entity in your application. 

In this first part, we’ll introduce the actor framework and dive into its fundamental concepts. Our focus will be on explaining the basics of the actor systems, alongside creating, and illustrating the purpose and usage of a few simple actors. 

The actor model timeline started in the early 70s where it was a theoretical model, and in the 80s it was the first implementation. Nowadays there are a few actor model frameworks for many languages. 

What is this magical framework?  

Akka is built on the Actor Model principles for building concurrent and distributed systems, allowing developers to manage concurrency effectively. In the actor model, every component is a unit called “actor”, and operates independently with its own state, behaviour, and communication capabilities.  

Here are some of the key ideas behind using the actor system in Akka.Net:

  1. Concurrency Management: Actors in Akka.Net provide a way to handle concurrency. This framework has a mailbox, and the messages are processed sequentially, as they arrive in the Actor’s mailbox. As it is right now, it implies that the actor-based system can only be sequential. It can block the execution of the next message, possibly blocking the system.  
  2. Isolation: Each actor encapsulates its state, and interactions with other actors occur only through message passing. This isolation specifies the management of state and reduces the concurrency related issues.  
  3. Location Transparency: Akka.Net supports distribution, meaning the actors can reside on different nodes in a network. The programmer does not need to be aware of the physical location of actors, as the actor system abstracts away the details of communication between distributed actors.  
  4. Fault Tolerance: Akka.Net provides tools for building fault-tolerant systems. Actors can be supervised, and supervisors can define how to handle failures within a specific hierarchy. If an actor fails, their supervisor can take corrective actions, such as restarting the actor or their parent.  
  5. Resilient: In correspondence to fault tolerance, actor systems are resilient in an automated way of recovering their failed component, by finding a strategy for repairing or isolating the component without compromising the system.  
  6. Scalability: The actor model is well-suited for building scalable systems. Actors can be distributed across multiple nodes. The message-passing nature of actors also supports asynchronous and non-blocking communication, which contributes to scalability.  
  7. Event-Driven Programming: Akka.Net encourages an event-driven programming model, where actors react to messages and events. This asynchronous and message-driven approach is suitable for building responsive and resilient systems.  
  8. Command-Query Responsibility Segregation (CQRS) – Akka.Net supports CQRS via its actor model, allowing actors to separate handle commands (writes) and queries (reads). Separating commands and queries facilitates eventual consistency models, especially in distributed systems where immediate consistency is challenging. It clarifies data flow, improves reliability by isolating responsibilities, and enhances performance through independent optimisation of read and write models. 

Let’s dive into some more interesting practical part.

Setting Up

To leverage Akka.Net in your projects, start by installing the latest Akka.Net NuGet package. This installation equips you with the Akka.Actor namespace, enabling the creation of your first actor system and actors. 

sampleActorSystem = ActorSystem.Create(“MyFirstActorSystem”); 

Defining Actors 

There are several types of actors depending on the purpose of the application like Persistence Actor, Receive Actor, Untyped Actor, Remote Actor, Supervisor Actor, Router, and Proxy actors. The actors are organised in a hierarchical structure, where each actor has a unique address that identifies it within the distributed system. In this way actors can communicate at any time, even with remote actors. Actors have a parent, and a parent can have multiple child actors. This hierarchy allows for the creation of supervision strategies, where supervisor actors can define how to handle failures within its supervised actors.  

Within an actor’s constructor, you define how it should react to different message types using the Receive<TMessage> method. This method registers a handler that processes messages of the specified type. 

After establishing the actor system, we can proceed to instantiate the actors. There are several methods to create actors. 

  1. Direct instantiation using ActorOf 
    • Global: var actorRef = system.ActorOf(Props.Create(() => new MyActor()), “myActor”); 
    • Child: var childActorRef = Context.ActorOf(Props.Create(() => new MyChildActor()), “myChildActor”); 
      • Child actors are useful for hierarchical organisation and supervision.  
  2. Using actor props  
    • Props is an Akka.NET class that describes how to create an actor instance. Props can be used for more than just simple instantiation; they also allow for configuring actors in various ways. 
  3. From a type

var actorRef = system.ActorOf(Props.Create(typeof(MyActor)), “myActorByType”); 

In Akka.Net applications, managing actor references efficiently is crucial for clean architecture and code maintainability. A practical approach to achieve this is using an actor accessor pattern. This pattern involves encapsulating actor references within dedicated accessor classes. Below is an example of how this pattern can be implemented: 

public class TaskManagementActorAccessor : ITaskManagementActorAccessor 

    public IActorRef TaskManagerActor { get; private set; } 

    public TaskManagementActorAccessor(ActorSystem actorSystem) 

    { 

        TaskManagerActor = actorSystem.ActorOf(Props.Create(() => new    TaskManagerActor()), “taskManager”); 

    } 

Components requiring interaction with the TaskManagerActor would have the TaskManagementActorAccessor injected, utilising it to communicate with the actor. This pattern not only streamlines actor management, but also aligns with best practices in software design, promoting encapsulation, abstraction, and dependency inversion for better application architecture in Akka.Net based systems.

Here is an example of how a TaskManagerActor could be implemented to handle CreateTaskCommand messages: 

public class TaskManagerActor : ReceiveActor 

    public TaskManagerActor() 

    { 

        Receive<CreateTaskCommand>(command => 

        { 

            var task = new TaskItem 

            { 

                Title = command.Title, 

                Description = command.Description 

            }; 

            // Logic to add the task to the system 

            Sender.Tell(new CreateTaskCommand.OK(task)); 

        }); 

    } 

Understanding IActorRef

IActorRef serves as a reference to actors, enabling message sending through the actor system without direct interaction with the actor itself. This abstraction facilitates a decoupled architecture, allowing system components to communicate via well-defined interfaces. taskManagerActor in the example above is an instance of IActorRef. You can obtain an IActorRef either by creating an actor or by looking it up in the system. 

Messaging and Actor Communication 

Actors communicate by exchanging messages, which are typically POCO classes. These messages should be immutable to ensure thread safety and to prevent unintended side effects. The recommended naming convention to distinguish between commands (actions you want the actor to perform) and events (notifications about something that happened):  

  • Commands: Use imperative verbs, like CreateTaskCommand, MarkAsTaskCompletedCommand. 
  • Events: Use past tense verbs or descriptive nouns, like TaskCreatedEvent, TaskCompletedEvent. 

For instance, to enable task manager actor to create new tasks, you could define a message like so:

public record CreateTaskCommand(string Title, string Description)  

    public record OK(TaskItem Task) : ITaskCommandReply; 

public interface ITaskCommandReply { } 

Here, CreateTaskCommand encapsulates the necessary data for task creation, and the nested OK record provides a structure for the response, including the newly created task.  

Sending Messages: “Tell” vs. “Ask” 

Actors communicate through messages. The two most common ways of sending messages through actors are the Fire-and-Forget way by using the Tell method, and the Request-Response way by using the Ask method. Usually, because the actor system follows an asynchronous way of communication, the best practice is to use the Tell commands and only send the message, without waiting for the response. These messages received by an actor are in the actor’s mailbox – a queue for handling the messages, where the actor processes them one by one.   

Example of using “Ask”: 

public async Task<TaskItem> CreateTaskAsync(CreateTaskCommand command) 

    // Ask TaskManagerActor to create a new task 

    var reply = await _actorAccessor.TaskManagerActor.Ask<CreateTaskCommand.OK>(command); 

    return reply switch 

    { 

        CreateTaskCommand.OK success => success.Task, 

        _ => throw new InvalidOperationException(“Unexpected response from TaskManagerActor.”

    }; 

Example of using “Tell”: 

public void MarkTaskAsComplete(int taskId) 

    _actorAccessor.TaskManagerActor.Tell(new CompleteTaskCommand(taskId)); 

}  

In this Tell example, the operation is non-blocking, and the system does not wait for a confirmation from the actor, adhering to the fire-and-forget paradigm. 

Summary

To summarise this initial segment, Akka.Net’s actor systems empower developers to create highly concurrent, scalable, fault-tolerant, and responsive applications. Its strengths shine especially in scenarios where traditional concurrency models and architectures fall short, or when simplifying the construction of concurrent, high throughput, and low-latency systems. It finds optimal utility in applications requiring seamless communication, real-time notifications, traffic management, multiplayer gaming, Internet of Things implementations, certain transactional workflows, etc. 

This concludes our introduction to this framework. In our next blog post, we will dive into the realm of persistence. We will explore how to ensure the state of your actors is preserved beyond the lifetime of a single message, enabling your applications to handle crashes and restarts gracefully. This is crucial for building reliable systems that require long-term data storage and retrieval capabilities.