DEV Community

Cover image for Understanding the Actor Design Pattern: A Practical Guide to Build Actor Systems with Akka in Java
mohamed alaaeldin
mohamed alaaeldin

Posted on

Understanding the Actor Design Pattern: A Practical Guide to Build Actor Systems with Akka in Java

What is the Actor Model?
Overview of the Actor Model:

The Actor Model is a mathematical model for concurrent computation, introduced by Carl Hewitt in the 1970s. It views computation as a collection of independent entities called “actors” that communicate with each other by sending messages. Each actor has its own state, behavior, and a mailbox for receiving messages.
Key Characteristics:

Isolation: Actors operate independently, encapsulating their state and behavior.
Asynchronous Messaging: Communication between actors is achieved through asynchronous message passing.
Location Transparency: Actors can be distributed across different machines, providing a level of transparency regarding their physical location.
No Shared Memory: Actors do not share memory, avoiding the complexities of locks and shared data.
Enter fullscreen mode Exit fullscreen mode
  1. Key Concepts of the Actor Design Pattern Actors:

Actors are the fundamental units in the Actor Model. An actor is an autonomous entity that processes messages, maintains its own state, and can create new actors. Actors operate concurrently, making them well-suited for handling parallel and distributed tasks.
Messages:

Messages are the means of communication between actors. Actors send and receive messages asynchronously. Messages can contain data or instructions, allowing actors to exchange information and coordinate their activities.
Mailboxes:

Each actor has a mailbox that stores incoming messages. The actor processes messages sequentially, ensuring that its state changes in a controlled manner. Mailboxes help in managing the asynchrony of message passing.

Actor System:

An Actor System is the runtime environment that manages and coordinates actors. It provides the infrastructure for creating, scheduling, and supervising actors. Actor systems can span across multiple machines, forming distributed systems.

  1. Actor Design Pattern in Action abstraction Creating Actors:

In most programming languages that support the Actor Model, creating an actor involves defining a class or function representing the actor’s behavior. Actors are instantiated within an actor system.

class MyActor(Actor):
    def receive(self, message):
        # Actor behavior goes here
        pass

Enter fullscreen mode Exit fullscreen mode

Creating an instance of MyActor within an actor system

my_actor = actor_system.create_actor(MyActor)

Sending and Receiving Messages:

Actors communicate by sending and receiving messages. Sending a message to an actor is typically an asynchronous operation.

# Sending a message to an actor
my_actor.tell("Hello, Actor!")

Enter fullscreen mode Exit fullscreen mode
# Actor receiving and processing messages
class MyActor(Actor):
    def receive(self, message):
        print(f"Received message: {message}")
Enter fullscreen mode Exit fullscreen mode

Actor Lifecycle:

Actors have a lifecycle that includes creation, processing messages, and termination. Actor systems manage the lifecycle of actors and may provide hooks for initialization and cleanup.


class MyActor(Actor):
    def pre_start(self):
        # Initialization logic goes here
        pass

    def receive(self, message):
        # Actor behavior

    def post_stop(self):
        # Cleanup logic goes here
        pass
Enter fullscreen mode Exit fullscreen mode
  1. Concurrency and Parallelism with Actors Asynchronous Processing:

One of the strengths of the Actor Model is its support for asynchronous processing. Actors can continue processing messages while waiting for external events, improving overall system responsiveness.
Handling State:

Actors encapsulate their state, and state changes are managed through message processing. This isolation helps in avoiding shared mutable state, reducing the risk of concurrency issues.
Supervision and Fault Tolerance:

Actor systems often include mechanisms for supervision and fault tolerance. If an actor encounters an error, its supervisor can take corrective actions, such as restarting the actor or redirecting messages.

  1. Practical Implementation of the Actor Model Choosing an Actor Framework:

Several programming languages offer libraries or frameworks for implementing the Actor Model. Popular choices include Akka for Scala/Java, Erlang OTP for Erlang, and Pykka for Python. Choose a framework based on your language of choice and the specific requirements of your project.
Designing Actor Systems:

When designing an actor system, consider the nature of the problem you are solving and how actors can collaborate to achieve the desired functionality. Identify the actors, their responsibilities, and the messages they exchange.
Case Study: Building a Concurrent System:

Let’s consider a simple example where actors represent different components of an e-commerce system — one actor for managing inventory, another for processing orders, and a third for handling customer notifications. These actors collaborate to ensure a smooth flow of e-commerce operations.

class InventoryManager(Actor):
    def receive(self, message):
        # Process inventory-related messages
        pass

class OrderProcessor(Actor):
    def receive(self, message):
        # Process order-related messages
        pass

class NotificationHandler(Actor):
    def receive(self, message):
        # Process notification-related messages
        pass
Enter fullscreen mode Exit fullscreen mode
  1. Challenges and Best Practices Scalability Considerations:

Design your actor system with scalability in mind. Distribute actors strategically, and leverage features provided by the actor framework for scalability, such as load balancing and clustering.
Avoiding Deadlocks and Race Conditions:

Carefully design message flows to avoid deadlocks and race conditions. Use appropriate synchronization mechanisms when actors need to coordinate their actions or share resources.
Testing Actor Systems:

Testing concurrent systems can be challenging. Leverage testing tools provided by the actor framework, and design tests that cover various scenarios, including message failures, actor restarts, and system recovery.
facts

The Actor Design Pattern offers a compelling approach to building concurrent and distributed systems. By embracing the principles of isolation, asynchronous messaging, and location transparency, developers can design systems that are scalable, responsive, and fault-tolerant. As you delve deeper into the Actor Model, explore different actor frameworks, and apply these concepts to real-world scenarios, you’ll unlock the full potential of this powerful paradigm in modern software development.
Choosing language and development framework

there are many framework like “AKKA , vertx.io both support java and Scala ”also Erlang/OTP
Erlang is a programming language used to build massively scalable soft real-time systems with requirements on high availability

in the last few months douglas crockford (the one who invent JSON) announced his language “Misty ” Misty Programming Language built for actor design pattern
https://www.youtube.com/watch?v=vMDHpPN_p08

worked with frameworks like “AKKA , vertx.io” i will use AKKA for this article you can still apply this concept on other frameworks .
A Practical Guide to Building Actor Systems with Akka in Java
What is AKKA

AKKA is a powerful toolkit and runtime for building highly concurrent, distributed, and fault-tolerant systems. At the heart of Akka lies the Actor Model, a paradigm that simplifies the development of concurrent and distributed applications. This tutorial aims to provide a step-by-step guide to building actor systems using Akka in Java.

  1. Setting Up Your Project Adding Akka Dependency

Add the Akka dependency to your project’s build file. For Maven projects, include the following in your pom.xml:

<dependencies>
    <dependency>
        <groupId>com.typesafe.akka</groupId>
        <artifactId>akka-actor_2.13</artifactId>
        <version>2.6.16</version>
    </dependency>
</dependencies>
Enter fullscreen mode Exit fullscreen mode

For Gradle projects, add the following to your build.gradle:

dependencies {
    implementation 'com.typesafe.akka:akka-actor_2.13:2.6.16'
}
Enter fullscreen mode Exit fullscreen mode

Project Structure

Create a simple project structure with a main class to run your Akka actor system. For example:

src
└── main
    └── java
        └── com
            └── yourcompany
                └── yourproject
                    └── Main.java
Enter fullscreen mode Exit fullscreen mode
  1. Understanding Actors in Akka Actor Hierarchy

In Akka, actors form a hierarchy. Each actor has a parent and may have child actors. This hierarchical structure allows for efficient supervision and fault tolerance.
Defining an Actor

Create an actor by extending the AbstractActor class. Override the createReceive method to define the actor's behavior.

import akka.actor.AbstractActor;

public class MyActor extends AbstractActor {

    @Override
    public Receive createReceive() {
        return receiveBuilder()
                .match(String.class, message -> {
                    // Handle String messages
                    System.out.println("Received: " + message);
                })
                .build();
    }
}
Enter fullscreen mode Exit fullscreen mode

Actor Lifecycle

Actors have a lifecycle with methods like preStart, postStop, and preRestart that you can override for initialization, cleanup, and error handling.

@Override
public void preStart() {
    // Initialization logic
    System.out.println("Actor started");
}


@Override
public void postStop() {
    // Cleanup logic
    System.out.println("Actor stopped");
}
Enter fullscreen mode Exit fullscreen mode
  1. Creating and Communicating with Actors ActorRef and ActorSystem

Create an instance of the ActorSystem and use it to create an ActorRef for your actor. The ActorRef is the entry point for interacting with the actor.

import akka.actor.ActorRef;
import akka.actor.ActorSystem;

public class Main {
    public static void main(String[] args) {
        // Create an ActorSystem
        ActorSystem system = ActorSystem.create("MySystem");

        // Create an ActorRef for MyActor
        ActorRef myActor = system.actorOf(MyActor.props(), "myActor");

        // Send a message to the actor
        myActor.tell("Hello, Akka!", ActorRef.noSender());

        // Shutdown the system
        system.terminate();
    }
}
Enter fullscreen mode Exit fullscreen mode

Sending Messages

Use the tell method of the ActorRef to send messages to an actor.

// Sending a message to the actor

myActor.tell("Hello, Akka!", ActorRef.noSender());
Enter fullscreen mode Exit fullscreen mode

Receiving Messages

Handle incoming messages in the actor’s createReceive method.

@Override
public Receive createReceive() {
    return receiveBuilder()
            .match(String.class, message -> {
                // Handle String messages
                System.out.println("Received: " + message);
            })
            .build();
}
Enter fullscreen mode Exit fullscreen mode
  1. Concurrency and Asynchronous Processing Handling State

Actors encapsulate their state. Ensure that state modifications are done within the actor’s behavior.

private String internalState = "";

@Override
public Receive createReceive() {
    return receiveBuilder()
            .match(String.class, message -> {
                // Handle String messages and modify state
                internalState = message;
                System.out.println("State updated: " + internalState);
            })
            .build();
}

Enter fullscreen mode Exit fullscreen mode

Asynchronous Message Processing

Leverage Akka’s asynchronous nature for non-blocking message processing.

@Override
public Receive createReceive() {
    return receiveBuilder()
            .match(String.class, message -> {
                // Asynchronous processing
                getContext().getSystem().scheduler().scheduleOnce(
                        Duration.create(1, TimeUnit.SECONDS),
                        getSelf(),
                        "Delayed response",
                        getContext().getSystem().dispatcher(),
                        getSelf()
                );
            })
            .matchEquals("Delayed response", message -> {
                // Handle delayed response
                System.out.println("Received delayed response");
            })
            .build();
}
Enter fullscreen mode Exit fullscreen mode
  1. Supervision and Fault Tolerance Actor Supervision

Supervise actors by defining a supervisor strategy in the actor system.

import akka.actor.OneForOneStrategy;
import akka.actor.SupervisorStrategy;
import scala.concurrent.duration.Duration;

@Override
public SupervisorStrategy supervisorStrategy() {
    return new OneForOneStrategy(
            10, // maxNrOfRetries
            Duration.create("1 minute"), // withinTimeRange
            throwable -> SupervisorStrategy.restart() // decision
    );
}
Enter fullscreen mode Exit fullscreen mode

Handling Failure

Actors can handle failures by restarting, stopping, or escalating the error to their supervisor. Override the preRestart method to implement custom restart logic.

@Override
public void preRestart(Throwable reason, Option<Object> message) {
    // Custom restart logic
    System.out.println("Restarting actor due to: " + reason.getMessage());
    super.preRestart(reason, message);
}

Enter fullscreen mode Exit fullscreen mode
  1. Testing Akka Actor Systems Unit Testing Actors

Use Akka’s TestActorRef for unit testing individual actors.

import akka.actor.ActorSystem;
import akka.testkit.TestActorRef;

public class MyActorTest {
    private final ActorSystem system = ActorSystem.create();

    @Test
    public void testMyActor() {
        // Create a TestActorRef for MyActor
        TestActorRef<MyActor> testActorRef = TestActorRef.create(system, MyActor.props());

        // Send a message and assert the result
        testActorRef.tell("Test Message", ActorRef.noSender());
        // Perform assertions on actor's behavior or state
    }
}
Enter fullscreen mode Exit fullscreen mode

TestKit and TestActorRef

For more complex scenarios involving multiple actors and asynchronous communication, use Akka’s TestKit.

import akka.actor.ActorSystem;
import akka.testkit.javadsl.TestKit;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;

public class MyActorSystemTest {

    private static ActorSystem system;

    @BeforeClass
    public static void setup() {
        system = ActorSystem.create();
    }

    @AfterClass
    public static void teardown() {
        TestKit.shutdownActorSystem(system);
    }

    @Test
    public void testActorSystem() {
        new TestKit(system) {{
            // Test actor system interactions
            // Send messages and assert the results
        }};
    }
}

Enter fullscreen mode Exit fullscreen mode
  1. Practical Example: Building a Simple Actor System Designing the System

Let’s design a simple actor system where an OrderProcessor actor communicates with a PaymentProcessor actor.
Implementing Actors

Define the OrderProcessor and PaymentProcessor actors.

public class OrderProcessor extends AbstractActor {
    @Override
    public Receive createReceive() {
        return receiveBuilder()
                .match(String.class, message -> {
                    System.out.println("Order received: " + message);
                    // Forward the order to the PaymentProcessor
                    getContext().actorOf(Props.create(PaymentProcessor.class)).forward(message, getContext());
                })
                .build();
    }
}

public class PaymentProcessor extends AbstractActor {
    @Override
    public Receive createReceive() {
        return receiveBuilder()
                .match(String.class, message -> {
                    System.out.println("Processing payment for order: " + message);
                    // Simulate payment processing
                    System.out.println("Payment processed successfully.");
                })
                .build();
    }
}

Enter fullscreen mode Exit fullscreen mode

Running the System

Create an actor system and send an order message to the OrderProcessor.

public class Main {
    public static void main(String[] args) {
        // Create an ActorSystem
        ActorSystem system = ActorSystem.create("OrderProcessingSystem");

        // Create an ActorRef for OrderProcessor
        ActorRef orderProcessor = system.actorOf(Props.create(OrderProcessor.class), "orderProcessor");

        // Send an order message
        orderProcessor.tell("12345", ActorRef.noSender());

        // Shutdown the system
        system.terminate();
    }
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

This tutorial has provided a comprehensive guide to building actor systems with Akka in Java. By understanding the Actor Model, creating actors, handling concurrency, implementing fault tolerance, and testing, you are now equipped to build robust and scalable systems using Akka’s powerful toolkit. As you continue your journey with Akka, explore its documentation and features to unlock the full potential of this actor-based concurrency model in your projects.

Top comments (0)