Scala Akka Actors: Introduction to the Actor Model
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
// 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:
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:
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:
// 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:
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:
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
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
| Scenario | Use |
|---|---|
| Independent async HTTP/DB call | Future |
| Parallel computation without shared state | Future |
| Managing mutable state across requests | Actor |
| Message queuing / rate limiting | Actor |
| Distributed system communication | Actor (+ Akka Cluster) |
| Pub/Sub event streams | Actor 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.
