Actors using Scala & Akka — Part 1 : Introduction

Share on:

Since I am stuck at home due to Covid-19 pandemic. With nothing else to do, I thought the best use of time would be to continue my blog series about actors. In the last article, we saw what are actors, their properties and how these properties can be useful for writing concurrent and stateful applications. It was all theory. In this article, I will demonstrate how to write your first akka actor in scala using IntelliJ IDE. Hop on even if you don’t know scala.

IDE & Project Setup

Scala development can be done in many IDEs. We will use IntelliJ with scala plugin.

Scala plugin::medium

Let’s create a new scala project

New project::medium

Scala - SBT::medium

Project configuration::medium

If you are creating a scala project for the first time, it can take a few minutes for the project to load.

Loading project::medium

Let’s open the build.sbt file and see what’s inside

build.sbt::medium

build.sbt is the file where we store most of build configurations such as modules, module dependencies and external dependencies, scala version, scala compiler flags, etc. Right now the file contains very minimal information as you can see in the screenshot. Since we will be using akka library for actors, let’s add that dependency. Dependencies can be added by adding an entry into libraryDependencies collection.

1libraryDependencies += "com.typesafe.akka" %% "akka-actor-typed" % "2.6.5"

After you add the dependency, you need to reload the project so that sbt (scala build tool) can download the new dependencies. Download usually takes 10–20 seconds depending on your bandwidth and which library you are downloading.

reload project::medium

We will need to reload the project every time we add or modify dependencies. We are now ready to write our code!

Main.scala

Let’s create a new package in scr/main/scala/ directory and then a Main file

::medium ::medium Main.scala::medium

Notice the little green triangle near Main object, you can click on it to run the Main program.

1object Main extends App {
2    println("hello scala")
3}

output:

1hello scala
2Process finished with exit code 0

“Main” here, is an object and not a class. In scala, an object is a “singleton instance” of a class created created for you automatically. We are now ready to write out first actor!

Running from CLI

To compile and run program from CLI instead of IntelliJ, install sbt. SBT can be installed on macos, windows and linux. After installing sbt, run “sbt” command to enter sbt shell and then execute “run” command.

Running from CLI

Running for the the first time, can take a while.

Our first actor : A bank account

Our first actor will be a representing a bank account. We will begin very simple and keep adding custom requirements as we go on to demonstrate akka actor capabilities.

Protocol

Our bank account has an account id and balance. Since account id can not change, only balance will be part of state and account id can be the “id” of the actor. The “behaviour” of actor will tree operations

  1. Deposit
  2. Withdraw
  3. PrintBalance

To support these operations, our actor will need to accept 3 “types” of messages. Let’s create them:

1sealed trait BankAccountMessage
2
3case class Deposit(amount: Int)  extends BankAccountMessage
4case class Withdraw(amount: Int) extends BankAccountMessage
5case object PrintBalance         extends BankAccountMessage

We will store these in BankAccountMessage.scala file in the same package.

Traits in scala are like interfaces in other languages with slight differences. Case classes in scala are used to hold or carry data to functions. They are immutable. Since PrintBalance does not need any variable, it can be made case object thus making it singleton. We have just defined protocol of our actor. Protocol includes what an actor can accept and what it can return. This actor does not return anything for now. Let’s now define the behaviour.

Behaviour

The behaviour is description of what an actor does when a new message arrives. It’s easy to think of behaviour as a function. A function which takes a message as an input parameter and returns a new behaviour. Why a new behaviour? Well, state is also part of the behaviour, so if a message wants to make a change to state, then you need to return a new behaviour with modified state. We will understand this more in the demo below.

One of the many ways to create a behaviour is via Behaviors.receiveMessage function. Since it create’s behaviour, let’s call it behaviour factory. It comes from akka.actor.typed.scaladsl.Behaviors package. Let’s observe the signature of this behaviour factory.

Behaviour factory signature::medium

It takes 1 type parameter which will enforce compile time type safety. The actor will only be able to receive given type of messages. The behaviour factory also accepts another parameter which is a function. Notice the function parameter notation in scala A=>B . I love this as it is very easy to read compared to Func<A,B> in C# and creating a new interface in java.

Here’s the behaviour of our bank account actor

1def behavior(balance: Int): Behavior[BankAccountMessage] = Behaviors.receiveMessage { message => 
2  message match {
3    case deposit:Deposit  => behavior(balance + deposit.amount)
4    case withdraw:Withdraw(amount) => behavior(balance - withdraw.amount)
5    case PrintBalance =>
6        println(s"balance = $balance")
7        behavior(balance)
8  }
9}

Some points for people who are new to scala:

  • ‘def’ is used to define methods
  • here we have used pattern matching over “message” and perform message type specific operation in each ‘case’

This code, however is not idiomatic scala. We can make it more concise. Scala allows reducing the code by allowing to remove message => message match making it more concise.

1def behavior(balance: Int): Behavior[BankAccountMessage] = Behaviors.receiveMessage {
2  case deposit:Deposit  => behavior(balance + deposit.amount)
3  case withdraw:Withdraw(amount) => behavior(balance - withdraw.amount)
4  case PrintBalance =>
5    println(s"balance = $balance")
6    behavior(balance)
7}

Scala also allows much more powerful pattern matching where we don’t have to create variables for deposit and withdraw. Instead, we can directly map variables to their fields.

1def behavior(balance: Int): Behavior[BankAccountMessage] = Behaviors.receiveMessage {
2  case Deposit(amount)  => behavior(balance + amount)
3  case Withdraw(amount) => behavior(balance - amount)
4  case PrintBalance =>
5    println(s"balance = $balance")
6    behavior(balance)
7}

If we get Deposit message, we simply return a new behaviour with added amount as the new balance (new state). We are essentially avoiding any variable mutations here by using recursion. Similar for Withdraw message. If we get a PrintBalance message, we print current balance and return the same behaviour with same state. We can also use Behaviors.same instead of behavior(balance).

1case PrintBalance =>
2  println(s"balance = $balance")
3  Behaviors.same

Spawning actor

Note that so far we have only described an actor using its protocol, behaviour and state. We still need to “spawn” the actor.

Spawning a new actor requires an actor system. Its like a runtime for all your actors within a JVM. In most cases you will not need more than one actor system in your entire application. Creating ActorSystem will require a guardian behaviour. With guardian behaviour, actor system creates a top level (system level) actor and you can send messages to the guardian actor which will then create child actors at user level. Akka recommends creating all business related actors at user level. This is because if a system actor crashes due an exception, entire actor system will crash destroying all other system and user actors.

In summary, a system actor is an actor spawned using actor system context and a user actor is an actor spawned using an actor context.

Actor Hierarchy::medium

Actor system takes a guardian behaviour in the constructor and behaves like a system level actor. In other words, you can start sending messages to actor system and they will be handled by guardian actor.

1//val is used to create "immutable" variables
2//scala also supports `var` which allows changing value of the variable after it a value was assigned to it
3
4val actorSystem = ActorSystem(guardianBehavior = ???, name = "MyBankActorSystem")

Right now, for guardianBehavior parameter, we can pass Behaviours.empty or behaviour(balance = 0) or SpawnProtocol(). SpawnProtocol() is the recommended way because it creates hierarchy of actors and creates domain actors in user space. But get to it later and pass Behaviours.empty for now.

1ActorSystem(Behaviours.empty, name = "MyBankActorSystem")

Now using this actor system, we will to spawn an (system) actor using bank account behaviour.

1val account1: ActorRef[BankAccountMessage] = 
2    actorSystem.systemActorOf(behavior(balance = 0), "account1")

Congratulations! 🥳 🎉 you have successfully created your first actor. Notice the type of account1. It is ActorRef[BankAccountMessage]. An actor ref is a pointer to an actual actor and all communication with actor has to be done via this actor ref. This is like the address of an actor. The actor also has a name and this name must be unique. In this case we have used account id for actor name. If you print string representation of account1 actor ref, you can see this name.

1println(account1)

output:

1Actor[akka://MyBankActorSystem/system/account1#-822693343]

Sending messages

Since this actor ref is “typed” we can only send messages of type BankAccountMessage to it. The syntax to send a message is !. Let’s send some messages.

1account1 ! PrintBalance
2account1 ! Deposit(200)
3account1 ! Withdraw(50)
4account1 ! PrintBalance

output:

1balance = 0
2balance = 150

End of Part 1

Here’s the entire Main.scala contents. All the project code can be found on github.

 1package tech.bilal
 2
 3import akka.actor.typed.scaladsl.Behaviors
 4import akka.actor.typed.{ActorRef, ActorSystem, Behavior}
 5
 6object Main extends App {
 7
 8  def behavior(balance: Int): Behavior[BankAccountMessage] = Behaviors.receiveMessage {
 9    case Deposit(amount)  => behavior(balance + amount)
10    case Withdraw(amount) => behavior(balance - amount)
11    case PrintBalance =>
12      println(s"balance = $balance")
13      behavior(balance)
14  }
15
16  val actorSystem                            = ActorSystem(Behaviors.empty, name = "MyBankActorSystem")
17  val account1: ActorRef[BankAccountMessage] = actorSystem.systemActorOf(behavior(balance = 0), "account1")
18  println(account1)
19
20  account1 ! PrintBalance
21  account1 ! Deposit(200)
22  account1 ! Withdraw(50)
23  account1 ! PrintBalance
24
25  actorSystem.terminate()
26}

In the next parts, we will do more things such as returning values from actors, spawn protocol, validations, async IO, failure handling, finite state machines, etc.