Scala Akka Actors: Introduction to the Actor Model

TT

Scala Akka Actors: Introduction to the Actor Model

Akka is Scala's toolkit for building concurrent, distributed, and resilient systems using the Actor Model. Actors are lightweight objects that communicate exclusively by message passing — no shared state, no locks. This model makes it easier to build correct concurrent systems at scale. This module introduces Akka Classic and Akka Typed.

The Actor Model

In the Actor Model:

  • Each actor is an independent unit with its own private state
  • Actors communicate only by sending messages (no shared memory)
  • Messages are processed one at a time — no race conditions within an actor
  • Actors can create children, send messages, and change behavior

This eliminates most concurrency bugs: since actors have private state and process one message at a time, you don't need locks or synchronization.

Adding Akka to Your Project

scala
// build.sbt
val akkaVersion = "2.8.5"

libraryDependencies ++= Seq(
  "com.typesafe.akka" %% "akka-actor-typed"   % akkaVersion,
  "com.typesafe.akka" %% "akka-actor-classic"  % akkaVersion,
  "ch.qos.logback"     % "logback-classic"     % "1.4.11"
)

Akka Classic Actors

The classic API uses Actor trait and untyped messages:

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

// Define message types
case class Greet(name: String)
case class Greeting(message: String)

// Define the actor
class GreeterActor extends Actor {
  def receive: Receive = {
    case Greet(name) =>
      println(s"Hello, $name!")
      sender() ! Greeting(s"Hello, $name!")

    case _ =>
      println("Unknown message")
  }
}

// Create the actor system and actor
val system = ActorSystem("MySystem")
val greeter: ActorRef = system.actorOf(Props[GreeterActor](), "greeter")

// Send a message (fire and forget)
greeter ! Greet("Alice")

// Shutdown
system.terminate()

Akka Typed Actors

Akka Typed (the modern API) makes message types explicit at compile time:

scala
import akka.actor.typed.{ActorRef, ActorSystem, Behavior}
import akka.actor.typed.scaladsl.Behaviors

// Message protocol
sealed trait GreeterCommand
case class Greet(name: String, replyTo: ActorRef[Greeting]) extends GreeterCommand
case class Greeting(message: String)

// Actor behavior
val greeterBehavior: Behavior[GreeterCommand] = Behaviors.receiveMessage {
  case Greet(name, replyTo) =>
    replyTo ! Greeting(s"Hello, $name!")
    Behaviors.same  // keep same behavior
}

// Create the system (the root actor IS the system in Typed Akka)
val system: ActorSystem[GreeterCommand] = ActorSystem(greeterBehavior, "MySystem")

Typed actors prevent sending the wrong message type — the compiler catches it.

Stateful Actors

Actors maintain state through their instance variables (Classic) or through the context.become mechanism:

scala
// Classic stateful actor
class CounterActor extends Actor {
  private var count = 0

  def receive: Receive = {
    case "increment" => count += 1
    case "get"       => sender() ! count
    case "reset"     => count = 0
  }
}

// Typed stateful actor
object CounterActor {
  sealed trait Command
  case object Increment extends Command
  case class Get(replyTo: ActorRef[Int]) extends Command

  def behavior(count: Int = 0): Behavior[Command] = Behaviors.receiveMessage {
    case Increment        => behavior(count + 1)
    case Get(replyTo)     => replyTo ! count; Behaviors.same
  }
}

In Typed Akka, state is passed as a parameter to a recursive behavior function — no mutable variables needed.

Ask Pattern: Request-Response

The ! operator sends fire-and-forget. The ask pattern (?) sends a message and returns a Future with the reply:

scala
import akka.pattern.ask
import akka.util.Timeout
import scala.concurrent.duration._

implicit val timeout: Timeout = Timeout(5.seconds)

val future = greeter ? Greet("Bob")

import scala.util.{Success, Failure}
future.onComplete {
  case Success(Greeting(msg)) => println(s"Got reply: $msg")
  case Failure(e)             => println(s"Failed: ${e.getMessage}")
}

Supervision: "Let It Crash"

Akka's supervision model makes fault tolerance explicit. Every actor has a supervisor (its parent) that decides what to do when it fails:

scala
import akka.actor.SupervisorStrategy._
import akka.actor.{OneForOneStrategy, Actor}

class Supervisor extends Actor {
  override val supervisorStrategy = OneForOneStrategy(
    maxNrOfRetries = 3,
    withinTimeRange = 1.minute
  ) {
    case _: ArithmeticException  => Resume    // keep state, resume
    case _: NullPointerException => Restart   // reset state, restart
    case _: Exception            => Escalate  // pass to parent supervisor
  }

  def receive: Receive = {
    case props: Props => sender() ! context.actorOf(props)
  }
}

Strategies:

  • Resume — ignore the exception, continue processing
  • Restart — recreate the actor (loses state)
  • Stop — terminate the actor
  • Escalate — forward to the parent's supervisor

Actor Lifecycle

scala
class LifecycleActor extends Actor {
  override def preStart(): Unit = println("Actor starting")
  override def postStop(): Unit = println("Actor stopped")
  override def preRestart(reason: Throwable, message: Option[Any]): Unit = {
    println(s"Restarting due to: ${reason.getMessage}")
    super.preRestart(reason, message)
  }

  def receive: Receive = {
    case msg => println(s"Received: $msg")
  }
}

When to Use Actors vs Futures

ScenarioUse
Independent async HTTP/DB callFuture
Parallel computation without shared stateFuture
Managing mutable state across requestsActor
Message queuing / rate limitingActor
Distributed system communicationActor (+ Akka Cluster)
Pub/Sub event streamsActor or Akka Streams

Frequently Asked Questions

Q: What is the difference between Akka Classic and Akka Typed? Akka Classic uses Actor trait with an untyped receive: Receive (which is Any => Unit). Any message can be sent to any actor — there's no compile-time check. Akka Typed introduces explicit message types: Behavior[Command] means the actor only processes Command messages. The compiler catches wrong message types. Typed also removes the sender() method (which caused bugs) in favor of explicit replyTo references. Typed is the recommended API for new projects.

Q: How many actors can I create in Akka? Akka actors are lightweight — each consumes only a few hundred bytes of memory. You can easily create millions of actors on a single JVM. This is fundamentally different from threads (which use ~1MB of stack each). This makes actors suitable for modeling per-user state, per-connection state, or any fine-grained concurrent entity where creating a thread per entity would be impractical.

Q: Does Akka replace Scala Futures or vice versa? They complement each other. Futures are best for stateless, short-lived async operations. Actors are best for stateful, long-lived concurrent entities. In practice, Akka applications use both: actors manage state and coordinate work, while Futures handle individual async operations within actor message handlers. The ask pattern (?) bridges the two by wrapping an actor message exchange in a Future.


Part of Scala Mastery Course — Module 20 of 22.