Skip to Content
Course content

33: Introduction to Akka Actors

Click on the "Edit" button in the top corner of the screen to edit your slide content.

If you're coming from a Java or standard Scala background, you likely have a mental model of "concurrency" as multiple threads accessing the same object and fighting over a lock. Because of this, the most common misconception I see when people start with Akka is thinking that an Actor is just a fancy object that happens to run on its own thread. You might think that calling a method on an Actor is just "automatic" asynchronous execution.

Actors aren't just objects with threads

Let's look at why that mental model fails. Imagine we're building a simple bank account. In a traditional object-oriented approach, you'd have a BankAccount class with a var balance. To make it thread-safe, you'd use synchronized or an AtomicInteger. When you call account.deposit(100), the code inside that method executes on your current thread. You are reaching inside the object and changing its state.

// The "Wrong" Way (Traditional Shared State)
class BankAccount {
  private var balance = 0
  def deposit(amount: Int): Unit = synchronized {
    balance += amount
  }
}

val account = new BankAccount()
// I am calling this from my thread, forcing the account to change state
account.deposit(100) 

In Akka, this is forbidden. If you try to treat an Actor like a regular object, you'll find you can't even access its internal methods. This is intentional. If Actors were just objects on threads, we'd still be dealing with race conditions, deadlocks, and the nightmare of managing locks across a distributed system.

The Mailbox: Why we "tell" instead of "call"

The correction is this: An Actor is not an object you call; it is a state machine that processes a queue of messages. You never actually interact with the Actor instance itself. Instead, you interact with an ActorRef—a handle or an "address" for that actor.

When you send a message to an Actor using the ! (tell) operator, you aren't executing a function. You are dropping a letter into the Actor's mailbox and immediately moving on with your life. The Actor then checks its mailbox and processes messages one by one, sequentially. Because the Actor only ever handles one message at a time, the code inside the Actor is effectively single-threaded. I love this because it means you can use a plain var balance inside an Actor without any synchronized blocks or volatile keywords, and it's perfectly safe.

import akka.actor.{Actor, ActorSystem, Props}

// Define the messages the Actor can understand
case class Deposit(amount: Int)
case object GetBalance

class BankAccountActor extends Actor {
  var balance = 0 // Safe! No locks needed because only one message is processed at a time.

  def receive = {
    case Deposit(amount) => 
      balance += amount
      println(s"Deposited $amount. New balance: $balance")
    case GetBalance => 
      sender() ! balance
  }
}

val system = ActorSystem("BankSystem")
val account = system.actorOf(Props[BankAccountActor], "myAccount")

// This doesn't "call" a method. It sends a message to the mailbox.
account ! Deposit(100) 
account ! Deposit(50)

Designing your protocol with Case Classes

Since you can't call methods, the "API" of your Actor is defined by the messages it accepts. In the Scala world, we use case classes and case objects for this. I call this the "Protocol."

If you find yourself sending generic String messages or Any, stop immediately. You lose all type safety and your code becomes a debugging nightmare. Always define a specific set of case classes that represent the intent of the communication. If the Actor doesn't recognize a message, it simply ignores it (or puts it in a "dead letters" queue), which prevents the entire system from crashing just because one component sent a malformed request.




📋 Practical Task

Build a Distributed Temperature Monitor

Your task is to implement a simple system where a TemperatureSensor actor sends readings to a TemperatureMonitor actor. The monitor should keep track of the maximum temperature recorded so far.

Requirements:

  • Create a TemperatureReading case class that holds a sensorId: String and a value: Double.
  • Create a GetMaxTemp case object.
  • Implement the TemperatureMonitor actor:
    • It should maintain a private var maxTemp (initialized to 0.0).
    • When it receives a TemperatureReading, it should update maxTemp if the new value is higher.
    • When it receives a GetMaxTemp message, it should send the current maxTemp back to the sender.
  • In your main application:
    • Spin up the TemperatureMonitor.
    • Send three different TemperatureReading messages (e.g., 22.5, 31.2, 18.9) to the monitor.
    • Send a GetMaxTemp message and print the result.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.