Learning Pekko Typed from Classic

Pekko Classic is the original Actor APIs, which have been improved by more type safe and guided Actor APIs, known as Pekko Typed.

If you already know the classic Actor APIs and would like to learn Pekko Typed, this reference is a good resource. Many concepts are the same and this page tries to highlight differences and how to do certain things in Typed compared to classic.

You should probably learn some of the basics of Pekko Typed to see how it looks like before diving into the differences and details described here. A good starting point for that is the IoT example in the Getting Started Guide or the examples shown in Introduction to Actors.

Note that Pekko Classic is still fully supported and existing applications can continue to use the classic APIs. It is also possible to use Pekko Typed together with classic actors within the same ActorSystem, see coexistence. For new projects we recommend using the new Actor APIs.

Dependencies

The dependencies of the Typed modules are named by adding -typed suffix of the corresponding classic module, with a few exceptions.

For example pekko-cluster-typed:

sbt
val PekkoVersion = "1.0.3"
libraryDependencies += "org.apache.pekko" %% "pekko-cluster-typed" % PekkoVersion
Maven
<properties>
  <scala.binary.version>2.13</scala.binary.version>
</properties>
<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.apache.pekko</groupId>
      <artifactId>pekko-bom_${scala.binary.version}</artifactId>
      <version>1.0.3</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>
<dependencies>
  <dependency>
    <groupId>org.apache.pekko</groupId>
    <artifactId>pekko-cluster-typed_${scala.binary.version}</artifactId>
  </dependency>
</dependencies>
Gradle
def versions = [
  ScalaBinary: "2.13"
]
dependencies {
  implementation platform("org.apache.pekko:pekko-bom_${versions.ScalaBinary}:1.0.3")

  implementation "org.apache.pekko:pekko-cluster-typed_${versions.ScalaBinary}"
}

Artifact names:

Classic Typed
pekko-actor pekko-actor-typed
pekko-cluster pekko-cluster-typed
pekko-cluster-sharding pekko-cluster-sharding-typed
pekko-cluster-tools pekko-cluster-typed
pekko-distributed-data pekko-cluster-typed
pekko-persistence pekko-persistence-typed
pekko-stream pekko-stream-typed
pekko-testkit pekko-actor-testkit-typed

Cluster Singleton and Distributed Data are included in pekko-cluster-typed.

Artifacts not listed in above table don’t have a specific API for Pekko Typed.

Package names

The convention of the package names in Pekko Typed is to add typed.scaladsl and typed.javadsl to the corresponding Pekko classic package name. scaladsl and javadsl is the convention to separate Scala and Java APIs, which is familiar from Pekko Streams.

Examples of a few package names:

Classic Typed for Scala Typed for Java
org.apache.pekko.actor org.apache.pekko.actor.typed.scaladsl org.apache.pekko.actor.typed.javadsl
org.apache.pekko.cluster org.apache.pekko.cluster.typed org.apache.pekko.cluster.typed
org.apache.pekko.cluster.sharding org.apache.pekko.cluster.sharding.typed.scaladsl org.apache.pekko.cluster.sharding.typed.javadsl
org.apache.pekko.persistence org.apache.pekko.persistence.typed.scaladsl org.apache.pekko.persistence.typed.javadsl

Actor definition

A classic actor is defined by a class extending org.apache.pekko.actor.Actororg.apache.pekko.actor.AbstractActor.

An actor in Typed is defined by a class extending org.apache.pekko.actor.typed.scaladsl.AbstractBehaviororg.apache.pekko.actor.typed.javadsl.AbstractBehavior.

It’s also possible to define an actor in Typed from functions instead of extending a class. This is called the functional style.

Classic HelloWorld actor:

Scala
sourceimport org.apache.pekko
import pekko.actor.Actor
import pekko.actor.ActorLogging
import pekko.actor.Props

object HelloWorld {
  final case class Greet(whom: String)
  final case class Greeted(whom: String)

  def props(): Props =
    Props(new HelloWorld)
}

class HelloWorld extends Actor with ActorLogging {
  import HelloWorld._

  override def receive: Receive = {
    case Greet(whom) =>
      log.info("Hello {}!", whom)
      sender() ! Greeted(whom)
  }
}
Java
sourceimport org.apache.pekko.actor.AbstractActor;
import org.apache.pekko.actor.Props;
import org.apache.pekko.event.Logging;
import org.apache.pekko.event.LoggingAdapter;

public class HelloWorld extends AbstractActor {

  public static final class Greet {
    public final String whom;

    public Greet(String whom) {
      this.whom = whom;
    }
  }

  public static final class Greeted {
    public final String whom;

    public Greeted(String whom) {
      this.whom = whom;
    }
  }

  public static Props props() {
    return Props.create(HelloWorld.class, HelloWorld::new);
  }

  private final LoggingAdapter log = Logging.getLogger(getContext().getSystem(), this);

  @Override
  public Receive createReceive() {
    return receiveBuilder().match(Greet.class, this::onGreet).build();
  }

  private void onGreet(Greet command) {
    log.info("Hello {}!", command.whom);
    getSender().tell(new Greeted(command.whom), getSelf());
  }
}

Typed HelloWorld actor:

Scala
sourceimport org.apache.pekko
import pekko.actor.typed.ActorRef
import pekko.actor.typed.Behavior
import pekko.actor.typed.scaladsl.AbstractBehavior
import pekko.actor.typed.scaladsl.ActorContext
import pekko.actor.typed.scaladsl.Behaviors

object HelloWorld {
  final case class Greet(whom: String, replyTo: ActorRef[Greeted])
  final case class Greeted(whom: String, from: ActorRef[Greet])

  def apply(): Behavior[HelloWorld.Greet] =
    Behaviors.setup(context => new HelloWorld(context))
}

class HelloWorld(context: ActorContext[HelloWorld.Greet]) extends AbstractBehavior[HelloWorld.Greet](context) {
  import HelloWorld._

  override def onMessage(message: Greet): Behavior[Greet] = {
    context.log.info("Hello {}!", message.whom)
    message.replyTo ! Greeted(message.whom, context.self)
    this
  }
}
Java
sourceimport org.apache.pekko.actor.typed.ActorRef;
import org.apache.pekko.actor.typed.Behavior;
import org.apache.pekko.actor.typed.javadsl.AbstractBehavior;
import org.apache.pekko.actor.typed.javadsl.ActorContext;
import org.apache.pekko.actor.typed.javadsl.Behaviors;
import org.apache.pekko.actor.typed.javadsl.Receive;

import java.util.HashMap;
import java.util.Map;

public class HelloWorld extends AbstractBehavior<HelloWorld.Greet> {

  public static final class Greet {
    public final String whom;
    public final ActorRef<Greeted> replyTo;

    public Greet(String whom, ActorRef<Greeted> replyTo) {
      this.whom = whom;
      this.replyTo = replyTo;
    }
  }

  public static final class Greeted {
    public final String whom;
    public final ActorRef<Greet> from;

    public Greeted(String whom, ActorRef<Greet> from) {
      this.whom = whom;
      this.from = from;
    }
  }

  public static Behavior<Greet> create() {
    return Behaviors.setup(HelloWorld::new);
  }

  private HelloWorld(ActorContext<Greet> context) {
    super(context);
  }

  @Override
  public Receive<Greet> createReceive() {
    return newReceiveBuilder().onMessage(Greet.class, this::onGreet).build();
  }

  private Behavior<Greet> onGreet(Greet command) {
    getContext().getLog().info("Hello {}!", command.whom);
    command.replyTo.tell(new Greeted(command.whom, getContext().getSelf()));
    return this;
  }
}

Why is it called Behavior and not Actor?

In Typed, the Behavior defines how to handle incoming messages. After processing a message, a different Behavior may be returned for processing the next message. This means that an actor is started with an initial Behavior and may change Behavior over its lifecycle. This is described more in the section about become.

Note that the Behavior has a type parameter describing the type of messages that it can handle. This information is not defined explicitly for a classic actor.

Links to reference documentation:

actorOf and Props

A classic actor is started with the actorOf method of the ActorContext or ActorSystem.

Corresponding method in Typed is called spawn in the org.apache.pekko.actor.typed.scaladsl.ActorContextorg.apache.pekko.actor.typed.javadsl.ActorContext.

There is no spawn method in the org.apache.pekko.actor.typed.scaladsl.ActorSystemorg.apache.pekko.actor.typed.javadsl.ActorSystem for creating top level actors. Instead, there is a single top level actor defined by a user guardian Behavior that is given when starting the ActorSystem. Other actors are started as children of that user guardian actor or children of other actors in the actor hierarchy. This is explained more in ActorSystem.

Note that when mixing classic and typed and have a classic system, spawning top level actors from the side is possible, see Coexistence.

The actorOf method takes an org.apache.pekko.actor.Props parameter, which is like a factory for creating the actor instance, and it’s also used when creating a new instance when the actor is restarted. The Props may also define additional properties such as which dispatcher to use for the actor.

In typed, the spawn method creates an actor directly from a given Behavior without using a Props factory. It does however accept an optional org.apache.pekko.actor.typed.Props for specifying Actor metadata. The factory aspect is instead defined via Behaviors.setup when using the object-oriented style with a class extending AbstractBehavior. For the function style there is typically no need for the factory.

Additional properties such as which dispatcher to use for the actor can still be given via an optional org.apache.pekko.actor.typed.Props parameter of the spawn method.

The name parameter of actorOf is optional and if not defined the actor will have a generated name. Corresponding in Typed is achieved with the spawnAnonymous method.

Links to reference documentation:

ActorRef

org.apache.pekko.actor.ActorRef has its correspondence in org.apache.pekko.actor.typed.ActorRef. The difference being that the latter has a type parameter describing which messages the actor can handle. This information is not defined for a classic actor and you can send any type of message to a classic ActorRef even though the actor may not understand it.

ActorSystem

org.apache.pekko.actor.ActorSystem has its correspondence in org.apache.pekko.actor.typed.ActorSystem. One difference is that when creating an ActorSystem in Typed you give it a Behavior that will be used as the top level actor, also known as the user guardian.

Additional actors for an application are created from the user guardian alongside performing the initialization of Pekko components such as Cluster Sharding. In contrast, in a classic ActorSystem, such initialization is typically performed from the “outside”.

The actorOf method of the classic ActorSystem is typically used to create a few (or many) top level actors. The ActorSystem in Typed doesn’t have that capability. Instead, such actors are started as children of the user guardian actor or children of other actors in the actor hierarchy. The rationale for this is partly about consistency. In a typed system you can’t create children to an arbitrary actor from anywhere in your app without messaging it, so this will also hold true for the user guardian actor. That noted, in cases where you do need to spawn outside of this guardian then you can use the SpawnProtocol to spawn as needed.

become

A classic actor can change its message processing behavior by using become in ActorContext. In Typed this is done by returning a new Behavior after processing a message. The returned Behavior will be used for the next received message.

There is no correspondence to unbecome in Typed. Instead you must explicitly keep track of and return the “previous” Behavior.

Links to reference documentation:

sender

There is no sender()getSender() in Typed. Instead you have to explicitly include an ActorRef representing the sender—or rather representing where to send a reply to—in the messages.

The reason for not having an implicit sender in Typed is that it wouldn’t be possible to know the type for the sender ActorRef[T]ActorRef<T> at compile time. It’s also much better to define this explicitly in the messages as it becomes more clear what the message protocol expects.

Links to reference documentation:

parent

There is no parentgetParent in Typed. Instead you have to explicitly include the ActorRef of the parent as a parameter when constructing the Behavior.

The reason for not having a parent in Typed is that it wouldn’t be possible to know the type for the parent ActorRef[T]ActorRef<T> at compile time without having an additional type parameter in the Behavior. For testing purposes it’s also better to pass in the parent since it can be replaced by a probe or being stubbed out in tests.

Supervision

An important difference between classic and typed is that in typed, actors are stopped by default if an exception is thrown and no supervision strategy is defined. In contrast, in classic, by default, actors are restarted.

In classic actors the supervision strategy for child actors are defined by overriding the supervisorStrategy method in the parent actor.

In Typed the supervisor strategy is defined by wrapping the Behavior of the child actor with Behaviors.supervise.

The classic BackoffSupervisor is supported by SupervisorStrategy.restartWithBackoff as an ordinary SupervisorStrategy in Typed.

SupervisorStrategy.Escalate isn’t supported in Typed, but similar can be achieved as described in Bubble failures up through the hierarchy.

Links to reference documentation:

Lifecycle hooks

Classic actors have methods preStart, preRestart, postRestart and postStop that can be overridden to act on changes to the actor’s lifecycle.

This is supported with corresponding PreRestart and PostStop signal messages in Typed. There are no PreStart and PostRestart signals because such action can be done from Behaviors.setup or the constructor of the AbstractBehavior class.

Note that in classic, the postStop lifecycle hook is also called when the actor is restarted. That is not the case in Typed, only the PreRestart signal is emitted. If you need to do resource cleanup on both restart and stop, you have to do that for both PreRestart and PostStop.

Links to reference documentation:

watch

watch and the Terminated message are pretty much the same, with some additional capabilities in Typed.

Terminated is a signal in Typed since it is a different type than the declared message type of the Behavior.

The watchWith method of the ActorContext in Typed can be used to send a message instead of the Terminated signal.

When watching child actors it’s possible to see if the child terminated voluntarily or due to a failure via the ChildFailed signal, which is a subclass of Terminated.

Links to reference documentation:

Stopping

Classic actors can be stopped with the stop method of ActorContext or ActorSystem. In Typed an actor stops itself by returning Behaviors.stopped. There is also a stop method in the ActorContext but it can only be used for stopping direct child actors and not any arbitrary actor.

PoisonPill is not supported in Typed. Instead, if you need to request an actor to stop you should define a message that the actor understands and let it return Behaviors.stopped when receiving that message.

Links to reference documentation:

ActorSelection

ActorSelection isn’t supported in Typed. Instead the Receptionist is supposed to be used for finding actors by a registered key.

ActorSelection can be used for sending messages to a path without having an ActorRef of the destination. Note that a Group Router can be used for that.

Links to reference documentation:

ask

The classic ask pattern returns a FutureCompletionStage for the response.

Corresponding ask exists in Typed and is good when the requester itself isn’t an actor. It is located in org.apache.pekko.actor.typed.scaladsl.AskPatternorg.apache.pekko.actor.typed.javadsl.AskPattern.

When the requester is an actor it is better to use the ask method of the ActorContext in Typed. It has the advantage of not having to mix FutureCompletionStage callbacks that are running on different threads with actor code.

Links to reference documentation:

pipeTo

pipeTo is typically used together with ask in an actor. The ask method of the ActorContext in Typed removes the need for pipeTo. However, for interactions with other APIs that return FutureCompletionStage it is still useful to send the result as a message to the actor. For this purpose there is a pipeToSelf method in the ActorContext in Typed.

ActorContext.children

The ActorContext has methods children and childgetChildren and getChild to retrieve the ActorRef of started child actors in both Typed and Classic.

The type of the returned ActorRef is unknown, since different types can be used for different children. Therefore, this is not a useful way to lookup children when the purpose is to send messages to them.

Instead of finding children via the ActorContext, it is recommended to use an application specific collection for bookkeeping of children, such as a Map[String, ActorRef[Child.Command]] Map<String, ActorRef<Child.Command>>. It can look like this:

Scala
sourceobject Parent {
  sealed trait Command
  case class DelegateToChild(name: String, message: Child.Command) extends Command
  private case class ChildTerminated(name: String) extends Command

  def apply(): Behavior[Command] = {
    def updated(children: Map[String, ActorRef[Child.Command]]): Behavior[Command] = {
      Behaviors.receive { (context, command) =>
        command match {
          case DelegateToChild(name, childCommand) =>
            children.get(name) match {
              case Some(ref) =>
                ref ! childCommand
                Behaviors.same
              case None =>
                val ref = context.spawn(Child(), name)
                context.watchWith(ref, ChildTerminated(name))
                ref ! childCommand
                updated(children + (name -> ref))
            }

          case ChildTerminated(name) =>
            updated(children - name)
        }
      }
    }

    updated(Map.empty)
  }
}
Java
sourcepublic class Parent extends AbstractBehavior<Parent.Command> {

  public interface Command {}

  public static class DelegateToChild implements Command {
    public final String name;
    public final Child.Command message;

    public DelegateToChild(String name, Child.Command message) {
      this.name = name;
      this.message = message;
    }
  }

  private static class ChildTerminated implements Command {
    final String name;

    ChildTerminated(String name) {
      this.name = name;
    }
  }

  public static Behavior<Command> create() {
    return Behaviors.setup(Parent::new);
  }

  private Map<String, ActorRef<Child.Command>> children = new HashMap<>();

  private Parent(ActorContext<Command> context) {
    super(context);
  }

  @Override
  public Receive<Command> createReceive() {
    return newReceiveBuilder()
        .onMessage(DelegateToChild.class, this::onDelegateToChild)
        .onMessage(ChildTerminated.class, this::onChildTerminated)
        .build();
  }

  private Behavior<Command> onDelegateToChild(DelegateToChild command) {
    ActorRef<Child.Command> ref = children.get(command.name);
    if (ref == null) {
      ref = getContext().spawn(Child.create(), command.name);
      getContext().watchWith(ref, new ChildTerminated(command.name));
      children.put(command.name, ref);
    }
    ref.tell(command.message);
    return this;
  }

  private Behavior<Command> onChildTerminated(ChildTerminated command) {
    children.remove(command.name);
    return this;
  }
}

Remember to remove entries from the Map when the children are terminated. For that purpose it’s convenient to use watchWith, as illustrated in the example above, because then you can include the key to the Map in the termination message. In that way the name of the actor doesn’t have to be the same as identifier used for bookkeeping.

Retrieving the children from the ActorContext can still be useful for some use cases, such as:

  • see if a child name is in use
  • stopping children
  • the type of the child is well known and unsafeUpcast of the ActorRef is considered “safe enough”

Remote deployment

Starting an actor on a remote node—so called remote deployment—isn’t supported in Typed.

This feature would be discouraged because it often results in tight coupling between nodes and undesirable failure handling. For example if the node of the parent actor crashes, all remote deployed child actors are brought down with it. Sometimes, that can be desired but many times it is used without realizing. This can be achieved by other means, such as using watch.

Routers

Routers are provided in Typed, but in a much simplified form compared to the classic routers.

Destinations of group routers are registered in the Receptionist, which makes them Cluster aware and also more dynamic than classic group routers.

Pool routers are only for local actor destinations in Typed, since remote deployment isn’t supported.

Links to reference documentation:

FSM

With classic actors there is explicit support for building Finite State Machines. No support is needed in Pekko Typed as it is straightforward to represent FSMs with behaviors.

Links to reference documentation:

Timers

In classic actors you mixin with Timersextend AbstractActorWithTimers to gain access to delayed and periodic scheduling of messages. In Typed you have access to similar capabilities via Behaviors.withTimers.

Links to reference documentation:

Stash

In classic actors you mixin with Stashextend AbstractActorWithStash to gain access to stashing of messages. In Typed you have access to similar capabilities via Behaviors.withStash.

Links to reference documentation:

PersistentActor

The correspondence of the classic PersistentActor is org.apache.pekko.persistence.typed.scaladsl.EventSourcedBehaviororg.apache.pekko.persistence.typed.javadsl.EventSourcedBehavior.

The Typed API is much more guided to facilitate Event Sourcing best practices. It also has tighter integration with Cluster Sharding.

Links to reference documentation:

Asynchronous Testing

The Test Kits for asynchronous testing are rather similar.

Links to reference documentation:

Synchronous Testing

Classic and typed have different Test Kits for synchronous testing.

Behaviors in Typed can be tested in isolation without having to be packaged into an actor. As a consequence, tests can run fully synchronously without having to worry about timeouts and spurious failures.

The BehaviorTestKit provides a nice way of unit testing a Behavior in a deterministic way, but it has some limitations to be aware of. Similar limitations exists for synchronous testing of classic actors.

Links to reference documentation: