Lesson 18 / 25

Actors with Apache Pekko and Akka

Understand the actor model and when to use it.

Message-driven concurrency

The actor model structures concurrent systems as actors: lightweight objects with private state that communicate only by asynchronous messages, processing one message at a time, so no locks are needed. Actors form supervision hierarchies: a parent decides whether to restart, resume or stop a failing child, which is the "let it crash" approach to resilience borrowed from Erlang. Akka popularised actors on the JVM, along with Akka Streams, clustering, sharding and persistence. In 2022, Akka moved to a source-available licence that requires a paid licence for many commercial uses, and the community created Apache Pekko, an Apache-licensed fork of Akka 2.6 with largely the same APIs under the org.apache.pekko package. Typed actors (Behavior[Message]) make the accepted messages part of the type. Use actors for stateful, long-lived entities (devices, sessions, game rooms, IoT), distributed systems with clustering and stream processing with backpressure. For simple request/response services, Futures or effect systems are usually simpler.

A typed actor holding cart state (Pekko)

State changes by returning a new behaviour; other actors reply via ActorRef.

import org.apache.pekko.actor.typed.{ActorRef, ActorSystem, Behavior}
import org.apache.pekko.actor.typed.scaladsl.Behaviors

object Cart:
  sealed trait Command
  final case class AddItem(sku: String, qty: Int) extends Command
  final case class GetTotal(replyTo: ActorRef[Int]) extends Command

  def apply(items: Map[String, Int] = Map.empty): Behavior[Command] =
    Behaviors.receiveMessage {
      case AddItem(sku, qty) =>
        val updated = items.updated(sku, items.getOrElse(sku, 0) + qty)
        Cart(updated)                      // next behaviour carries the new state
      case GetTotal(replyTo) =>
        replyTo ! items.values.sum         // reply asynchronously
        Behaviors.same
    }

object Printer:
  def apply(): Behavior[Int] = Behaviors.receiveMessage { total =>
    println(s"items in cart: $total")
    Behaviors.same
  }

@main def cartDemo(): Unit =
  val system = ActorSystem(Behaviors.setup[Unit] { ctx =>
    val cart = ctx.spawn(Cart(), "cart-asha")
    val printer = ctx.spawn(Printer(), "printer")
    cart ! Cart.AddItem("pen", 2)
    cart ! Cart.AddItem("ink", 1)
    cart ! Cart.GetTotal(printer)          // printer eventually prints: items in cart: 3
    Behaviors.empty
  }, "shop")

Check licences before choosing Akka

Since 2022, newer Akka versions are not under an open-source licence for most commercial production use. Many teams choose Apache Pekko for an Apache 2.0-licensed alternative with familiar APIs.

Quick check: How do actors avoid locks around their state?

  • Each actor processes one message at a time and its state is private, changed only by its own message handling
  • They use synchronized blocks
  • They never have state
  • They use global variables
Answer

Each actor processes one message at a time and its state is private, changed only by its own message handling — Encapsulated state plus sequential message processing removes the need for locks.