Documentation

§Integrating with Pekko Typed

Pekko 2.6 marked the new typed Actor API (“Pekko Typed”) as stable. The typed API is now officially the main API for Pekko. In the typed API, each actor needs to declares which message type it is able to handle and the type system enforces that only messages of this type can be sent to the actor. Although Play does not fully adopt Pekko Typed, we already provide some APIs to better integrate it in Play applications.

Note: the Pekko classic APIs are still fully supported and existing applications can continue to use them. There are no plans to deprecate or remove Pekko classic API.

§Pekko Actor Typed styles

Pekko’s Actor Typed API has two styles:

  1. a “functional programming” style, based on defining an actor Behaviors with values; and
  2. a “object-oriented” style, based on defining an actor Behaviors with subclasses.

For instance, here’s an example of a simple actor that says hello back:

Scala FP
import org.apache.pekko.actor.typed.scaladsl.Behaviors
import org.apache.pekko.actor.typed.ActorRef
import org.apache.pekko.actor.typed.Behavior

object HelloActor {
  final case class SayHello(
      name: String,
      replyTo: ActorRef[String],
  )

  def create(): Behavior[SayHello] = {
    Behaviors.receiveMessage[SayHello] {
      case SayHello(name, replyTo) =>
        replyTo ! s"Hello, $name"
        Behaviors.same
    }
  }
}
Scala OO
import org.apache.pekko.actor.typed.scaladsl.AbstractBehavior
import org.apache.pekko.actor.typed.scaladsl.ActorContext
import org.apache.pekko.actor.typed.scaladsl.Behaviors
import org.apache.pekko.actor.typed.ActorRef
import org.apache.pekko.actor.typed.Behavior

object HelloActor {
  final case class SayHello(
      name: String,
      replyTo: ActorRef[String],
  )

  def create(): Behavior[HelloActor.SayHello] = {
    Behaviors.setup(context => new HelloActor(context))
  }
}

final class HelloActor private (
    context: ActorContext[HelloActor.SayHello],
) extends AbstractBehavior(context) {
  import HelloActor._

  def onMessage(msg: SayHello): HelloActor = {
    msg.replyTo ! s"Hello, ${msg.name}"
    this
  }
}
Java FP
import org.apache.pekko.actor.typed.ActorRef;
import org.apache.pekko.actor.typed.Behavior;
import org.apache.pekko.actor.typed.javadsl.Behaviors;

public final class HelloActor {

  public static final class SayHello {
    public final String name;
    public final ActorRef<String> replyTo;

    public SayHello(String name, ActorRef<String> replyTo) {
      this.name = name;
      this.replyTo = replyTo;
    }
  }

  public static Behavior<HelloActor.SayHello> create() {
    return Behaviors.receiveMessage(
        (SayHello message) -> {
          message.replyTo.tell("Hello, " + message.name);
          return Behaviors.same();
        });
  }
}
Java OO
import 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;

public final class HelloActor extends AbstractBehavior<HelloActor.SayHello> {

  public static final class SayHello {
    public final String name;
    public final ActorRef<String> replyTo;

    public SayHello(String name, ActorRef<String> replyTo) {
      this.name = name;
      this.replyTo = replyTo;
    }
  }

  public static Behavior<HelloActor.SayHello> create() {
    return Behaviors.setup((ctx) -> new HelloActor(ctx));
  }

  private HelloActor(ActorContext<HelloActor.SayHello> context) {
    super(context);
  }

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

  private Behavior<SayHello> onHello(SayHello message) {
    message.replyTo.tell("Hello, " + message.name);
    return this;
  }
}

While here is an example of an actor that depends on Play’s Configuration in order to return configuration values:

Scala FP
import com.google.inject.Provides
import org.apache.pekko.actor.typed.scaladsl.Behaviors
import org.apache.pekko.actor.typed.ActorRef
import org.apache.pekko.actor.typed.Behavior
import play.api.libs.concurrent.ActorModule
import play.api.Configuration

object ConfiguredActor extends ActorModule {
  type Message = GetConfig

  final case class GetConfig(replyTo: ActorRef[String])

  @Provides
  def create(configuration: Configuration): Behavior[GetConfig] = {
    Behaviors.setup { _ =>
      val config = configuration.get[String]("my.config")
      Behaviors.receiveMessage[GetConfig] {
        case GetConfig(replyTo) =>
          replyTo ! config
          Behaviors.same
      }
    }
  }
}
Scala OO
import javax.inject.Inject

import org.apache.pekko.actor.typed.scaladsl.AbstractBehavior
import org.apache.pekko.actor.typed.scaladsl.ActorContext
import org.apache.pekko.actor.typed.scaladsl.Behaviors
import org.apache.pekko.actor.typed.ActorRef
import org.apache.pekko.actor.typed.Behavior
import play.api.Configuration

object ConfiguredActor {
  final case class GetConfig(replyTo: ActorRef[String])

  def create(
      configuration: Configuration,
  ): Behavior[ConfiguredActor.GetConfig] = {
    Behaviors.setup { context => new ConfiguredActor(context, configuration) }
  }
}

final class ConfiguredActor private (
    context: ActorContext[ConfiguredActor.GetConfig],
    configuration: Configuration,
) extends AbstractBehavior(context) {
  import ConfiguredActor._

  val config = configuration.get[String]("my.config")
  def onMessage(msg: GetConfig): ConfiguredActor = {
    msg.replyTo ! config
    this
  }
}
Java FP
import com.typesafe.config.Config;
import org.apache.pekko.actor.typed.ActorRef;
import org.apache.pekko.actor.typed.Behavior;
import org.apache.pekko.actor.typed.javadsl.Behaviors;

public final class ConfiguredActor {

  public static final class GetConfig {
    public final ActorRef<String> replyTo;

    public GetConfig(ActorRef<String> replyTo) {
      this.replyTo = replyTo;
    }
  }

  public static Behavior<ConfiguredActor.GetConfig> create(Config config) {
    String myConfig = config.getString("my.config");
    return Behaviors.receiveMessage(
        (GetConfig message) -> {
          message.replyTo.tell(myConfig);
          return Behaviors.same();
        });
  }
}
Java OO
import com.typesafe.config.Config;
import 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;

public final class ConfiguredActor extends AbstractBehavior<ConfiguredActor.GetConfig> {

  public static final class GetConfig {
    public final ActorRef<String> replyTo;

    public GetConfig(ActorRef<String> replyTo) {
      this.replyTo = replyTo;
    }
  }

  private final String config;

  public static Behavior<ConfiguredActor.GetConfig> create(Config config) {
    return Behaviors.setup((ctx) -> new ConfiguredActor(ctx, config));
  }

  private ConfiguredActor(ActorContext<ConfiguredActor.GetConfig> context, Config config) {
    super(context);
    this.config = config.getString("my.config");
  }

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

  private Behavior<GetConfig> onGetConfig(GetConfig message) {
    message.replyTo.tell(config);
    return this;
  }
}

§Dependency Injection

If your actor’s behavior has mutable state, as is sometimes common in the object-oriented style, make sure you don’t share the same Behavior instance for multiple ActorRefs. Here are some general ways to avoid the problem:

  1. Consider a design without mutable state;
  2. Don’t leak the Behavior instance by only exposing the ActorRef instance, for example by only binding the ActorRef;
  3. If the objective is to only have one single instance of the actor, then make sure that both the Behavior and ActorRef are singletons, for example by using @Singleton or .asEagerSingleton;
  4. If, instead, there are meant to be multiple instances of the same actor then make sure both Behavior and ActorRef are named singletons, in Guice by using @Named or .annotatedWith(Names.named(..)).

§Compile-time dependency injection

Using compile-time dependency injection for Pekko Actor Typed requires creating the actor Behavior value and using it to spawn the actor:

Scala
import org.apache.pekko.actor.typed.scaladsl.adapter._
import play.api._
import play.api.routing.Router

final class AppComponents(context: ApplicationLoader.Context)
    extends BuiltInComponentsFromContext(context)
    with NoHttpFiltersComponents {
  val router = Router.empty

  val helloActor = {
    actorSystem.spawn(HelloActor.create(), "hello-actor")
  }
  val configuredActor = {
    val behavior = ConfiguredActor.create(configuration)
    actorSystem.spawn(behavior, "configured-actor")
  }

  val main = new Main(helloActor, configuredActor)
}
Java
import java.util.Collections;
import java.util.List;
import org.apache.pekko.actor.typed.ActorRef;
import org.apache.pekko.actor.typed.javadsl.Adapter;
import play.ApplicationLoader;
import play.BuiltInComponentsFromContext;
import play.mvc.EssentialFilter;
import play.routing.Router;

public final class AppComponents extends BuiltInComponentsFromContext {

  public final ActorRef<HelloActor.SayHello> helloActor;
  public final ActorRef<ConfiguredActor.GetConfig> configuredActor;
  public final Main main;

  public AppComponents(ApplicationLoader.Context context) {
    super(context);
    helloActor = Adapter.spawn(actorSystem(), HelloActor.create(), "hello-actor");
    configuredActor =
        Adapter.spawn(actorSystem(), ConfiguredActor.create(config()), "configured-actor");
    main = new Main(helloActor, configuredActor);
  }

  @Override
  public Router router() {
    return Router.empty();
  }

  @Override
  public List<EssentialFilter> httpFilters() {
    return Collections.emptyList();
  }
}

§Runtime dependency injection

For runtime dependency injection use the “typed” methods in PekkoGuiceSupport, if using the functional-programming style. For the object-oriented style you must write a Provider for your ActorRef and bind it.

For instance, given a component in your application or system that needs injecting, like this one:

Scala
import javax.inject.Inject
import javax.inject.Singleton

import org.apache.pekko.actor.typed.ActorRef

@Singleton final class Main @Inject() (
    val helloActor: ActorRef[HelloActor.SayHello],
    val configuredActor: ActorRef[ConfiguredActor.GetConfig],
)
Java
import javax.inject.Inject;
import javax.inject.Singleton;
import org.apache.pekko.actor.typed.ActorRef;

@Singleton
public final class Main {
  public final ActorRef<HelloActor.SayHello> helloActor;
  public final ActorRef<ConfiguredActor.GetConfig> configuredActor;

  @Inject
  public Main(
      ActorRef<HelloActor.SayHello> helloActor,
      ActorRef<ConfiguredActor.GetConfig> configuredActor) {
    this.helloActor = helloActor;
    this.configuredActor = configuredActor;
  }
}

You can define a Guice Module like so:

Scala FP
import com.google.inject.AbstractModule
import play.api.libs.concurrent.PekkoGuiceSupport

object AppModule extends AbstractModule with PekkoGuiceSupport {
  override def configure() = {
    bindTypedActor(HelloActor.create(), "hello-actor")  // uses "create" method
    bindTypedActor(ConfiguredActor, "configured-actor") // uses the object itself
  }
}
Scala OO
import javax.inject.Inject

import com.google.inject.AbstractModule
import com.google.inject.Provider
import com.google.inject.TypeLiteral
import org.apache.pekko.actor.typed.scaladsl.adapter._
import org.apache.pekko.actor.typed.ActorRef
import org.apache.pekko.actor.ActorSystem
import play.api.libs.concurrent.PekkoGuiceSupport
import play.api.Configuration

object AppModule extends AbstractModule with PekkoGuiceSupport {
  override def configure() = {
    bindTypedActor(HelloActor.create(), "hello-actor")
    bind(new TypeLiteral[ActorRef[ConfiguredActor.GetConfig]]() {})
      .toProvider(classOf[ConfiguredActorProvider])
      .asEagerSingleton()
  }

  private class ConfiguredActorProvider @Inject() (
      actorSystem: ActorSystem,
      configuration: Configuration,
  ) extends Provider[ActorRef[ConfiguredActor.GetConfig]] {
    def get() = {
      val behavior = ConfiguredActor.create(configuration)
      actorSystem.spawn(behavior, "configured-actor")
    }
  }
}
Java FP
import com.google.inject.AbstractModule;
import com.google.inject.Provider;
import com.google.inject.TypeLiteral;
import com.typesafe.config.Config;
import javax.inject.Inject;
import org.apache.pekko.actor.ActorSystem;
import org.apache.pekko.actor.typed.ActorRef;
import org.apache.pekko.actor.typed.javadsl.Adapter;

public class AppModule extends AbstractModule {

  @Override
  protected void configure() {
    bind(new TypeLiteral<ActorRef<HelloActor.SayHello>>() {})
        .toProvider(HelloActorProvider.class)
        .asEagerSingleton();
    bind(new TypeLiteral<ActorRef<ConfiguredActor.GetConfig>>() {})
        .toProvider(ConfiguredActorProvider.class)
        .asEagerSingleton();
  }

  public static class HelloActorProvider implements Provider<ActorRef<HelloActor.SayHello>> {
    private final ActorSystem actorSystem;

    @Inject
    public HelloActorProvider(ActorSystem actorSystem) {
      this.actorSystem = actorSystem;
    }

    @Override
    public ActorRef<HelloActor.SayHello> get() {
      return Adapter.spawn(actorSystem, HelloActor.create(), "hello-actor");
    }
  }

  public static class ConfiguredActorProvider
      implements Provider<ActorRef<ConfiguredActor.GetConfig>> {

    private final ActorSystem actorSystem;
    private final Config config;

    @Inject
    public ConfiguredActorProvider(ActorSystem actorSystem, Config config) {
      this.actorSystem = actorSystem;
      this.config = config;
    }

    @Override
    public ActorRef<ConfiguredActor.GetConfig> get() {
      return Adapter.spawn(actorSystem, ConfiguredActor.create(config), "configured-actor");
    }
  }
}
Java OO
import com.google.inject.AbstractModule;
import com.google.inject.Provider;
import com.google.inject.TypeLiteral;
import com.typesafe.config.Config;
import javax.inject.Inject;
import org.apache.pekko.actor.ActorSystem;
import org.apache.pekko.actor.typed.ActorRef;
import org.apache.pekko.actor.typed.javadsl.Adapter;

public class AppModule extends AbstractModule {

  @Override
  protected void configure() {
    bind(new TypeLiteral<ActorRef<HelloActor.SayHello>>() {})
        .toProvider(HelloActorProvider.class)
        .asEagerSingleton();
    bind(new TypeLiteral<ActorRef<ConfiguredActor.GetConfig>>() {})
        .toProvider(ConfiguredActorProvider.class)
        .asEagerSingleton();
  }

  public static class HelloActorProvider implements Provider<ActorRef<HelloActor.SayHello>> {
    private final ActorSystem actorSystem;

    @Inject
    public HelloActorProvider(ActorSystem actorSystem) {
      this.actorSystem = actorSystem;
    }

    @Override
    public ActorRef<HelloActor.SayHello> get() {
      return Adapter.spawn(actorSystem, HelloActor.create(), "hello-actor");
    }
  }

  public static class ConfiguredActorProvider
      implements Provider<ActorRef<ConfiguredActor.GetConfig>> {

    private final ActorSystem actorSystem;
    private final Config config;

    @Inject
    public ConfiguredActorProvider(ActorSystem actorSystem, Config config) {
      this.actorSystem = actorSystem;
      this.config = config;
    }

    @Override
    public ActorRef<ConfiguredActor.GetConfig> get() {
      return Adapter.spawn(actorSystem, ConfiguredActor.create(config), "configured-actor");
    }
  }
}

§Using the AskPattern & Typed Scheduler

When interacting with actors from outside of another Actor, for example from a Controller, you need to use AskPattern.ask to send a message to the actor and get a response. The AskPattern.ask method requires a pekko.actor.typed.Scheduler that you can obtain via Dependency Injection.

§Runtime dependency injection

Runtime dependency injection works as any other runtime DI module in Play. The Scheduler is part of the default bindings, so the module is enabled automatically, and an instance is available for injection.

§Compile-time dependency injection

If you’re using compile-time DI, you can get have access to the Scheduler by using the components like below:

Java
import play.ApplicationLoader;
import play.BuiltInComponentsFromContext;
import play.components.PekkoTypedComponents;
import play.controllers.AssetsComponents;
import play.filters.components.HttpFiltersComponents;
import play.routing.Router;

public class ComponentsWithTypedScheduler extends BuiltInComponentsFromContext
    implements PekkoTypedComponents, AssetsComponents, HttpFiltersComponents {

  public ComponentsWithTypedScheduler(ApplicationLoader.Context context) {
    super(context);
  }

  @Override
  public Router router() {
    return Router.empty();
  }
}
Scala
import play.api.libs.concurrent.PekkoTypedComponents
import play.api.routing.Router
import play.api.Application
import play.api.ApplicationLoader
import play.api.ApplicationLoader.Context
import play.api.BuiltInComponentsFromContext
import play.filters.HttpFiltersComponents

class MyApplicationLoaderUsingTypedScheduler extends ApplicationLoader {
  override def load(context: Context): Application = {
    new ComponentsWithTypedScheduler(context).application
  }
}

class ComponentsWithTypedScheduler(context: Context)
    extends BuiltInComponentsFromContext(context)
    with HttpFiltersComponents
    with PekkoTypedComponents {
  override lazy val router: Router = Router.empty
}

Next: Pekko Cluster Sharding for Pekko Typed (incubating)


Found an error in this documentation? The source code for this page can be found here. After reading the documentation guidelines, please feel free to contribute a pull request. Have questions or advice to share? Go to our community forums to start a conversation with the community.