There is More To Akka-typed Than Meets the Eye


Recently, I took an online course on reactive programming with Scala and Akka. I had already been using akka for a while, but not the akka-typed module, so I was especially curious about it. And it struck me.

The theory

I was expecting a typed actor to be just like a classic akka actor with a type parameter that says what type of message it accepts, and they obviously added this type parameter to the ActorRef. But wait a minute, because as I later found out, there are a couple of perks that come with the upgrade.

The big conceptual change introduced by akka-typed is the Behavior[T] type. In akka-typed, in order to get an actor up and running, one must first define a behavior, and then spawn one or many actors that actually execute the defined behavior. A Behavior[T] can be seen roughly as a function that receives a parameter of type T (the message), reacts to the message, and returns another Behavior[T]. This is an elegant separation between functionality and execution context, and it automatically brings in a few benefits. Below I will list my three favorite ones.


The behavior can now be tested alone, independently from the execution context. For example, one can take a behavior and, without spawning an actor, “run” a message with that behavior. It seems more functional to me. Like taking a function and applying it to some parameters, abstracting away the context in which it’s executed. Not having to deal with the real-world problems of asynchronous communication makes testing more deterministic.

Error handling

Supervision was improved. When an actor fails, instead of it having to send a message to its parent and wait for a response (possibly over the network), the error handling is done locally. How? By defining the supervision strategy as a behavior wrapper. Behaviors are pretty much like functions, so a behavior can be wrapped like any function, catching any exceptions and handling them. The result is a new behavior like the original one, plus the error handling code. Once a child actor is spawned with this complete behavior, it will know what to do in case of failures. This is an elegant solution, because the wrapping can still be defined in the parent actor code, which decouples both the logic, by keeping the behavior function separated from the error handling, and the execution, by not having to send messages back and forth to know what to do about an error. Remember that regular messages can be used as part of the protocol if needed.

Protocol definition

Another big difference with akka classic, is the introduction of a protocol in the communication between actors. Because an actor’s reference ActorRef[T] is now parameterized, when it receives a message, there is no way to implicitly get the sender’s reference (it could be any type of actor who sent the message). This forces the sender to explicitly pass a reference in a replyTo field in the message sent, making the basis for the communication protocol. I find this concept interesting, because not only the type of messages that an actor expects can be declared in advance, but also the order in which the actor expects them. I will dive deeper into this in the following example.

The project

After finishing the course, eager to try akka-typed myself, I set out to create a pet project to try and see how far I could go in defining a compiler-checked protocol. I usually like to create projects related to music, and this one was no exception. So I modeled a tiny part of a music player: the play queue. I was mostly interested in the states it goes through, and which actions were valid in each state. For example, I can skip a track when I’m playing the first of a long queue of tracks, but I shouldn’t be able to skip ahead when I’m listening to the last queued track. Same as I shouldn’t be able to skip back when I’m playing the first track in the queue. The states and their transitions can be seen in the following diagram.

akka-typed graph
FSM representing the states and transitions of a player’s queue. States suffix “Track” was removed for simplicity, for example “FirstTrack” is shown here as just “First”.

This is a finite state machine (FSM) where each state represents the state of the player’s queue, and the arrows represent the possible messages that can be received along with the corresponding state transition. When the player is initialized, there are no tracks in the queue, so I called it Empty. Then I can add a track to the queue, and it will only have that one track, therefore I called this state OnlyTrack. Adding a second track or more, will move the FSM to the FirstTrack state, meaning it’s playing the first track but there are more tracks ahead. From this state, I am now able to skip the track, moving to MiddleTrack state if there are still more tracks remaining, or to LastTrack if there are only two tracks in the queue.

As mentioned before, using the correct types for the messages sent between the actors and the replyTo field of the responses, I create a protocol that gives me type safety not only in the type of messages exchanged but also in the order I use them.

Here are the messages accepted by the player (a.k.a. commands) and the corresponding responses. Note how for example EnqueueFirstTrack has a replyTo field which is a reference to an actor that expects FirstTrackEnqueued response, which in turn has a reference to an actor which accepts OnlyTrackCommand. Following this chain of messages and replyTo’s is how the protocol is defined.

object PlayerCommands {

  sealed trait PlayerCommand
  sealed trait MiddleTrackCommand extends PlayerCommand
  sealed trait EmptyCommand extends PlayerCommand
  sealed trait FirstTrackCommand extends PlayerCommand
  sealed trait LastTrackCommand extends PlayerCommand
  sealed trait OnlyTrackCommand extends PlayerCommand

  case class Skip(replyTo: ActorRef[SkippedReply])
    extends MiddleTrackCommand
    with FirstTrackCommand

  case class SkipBack(replyTo: ActorRef[SkippedBackReply])
    extends MiddleTrackCommand
    with LastTrackCommand

  case class EnqueueFirstTrack(track: Track, replyTo: ActorRef[FirstTrackEnqueued])
    extends EmptyCommand

  case class EnqueueSecondTrack(track: Track, replyTo: ActorRef[SecondTrackEnqueued])
    extends OnlyTrackCommand

  case class EnqueueTrack(track: Track, replyTo: ActorRef[TrackEnqueuedReply])
    extends MiddleTrackCommand
    with FirstTrackCommand
    with LastTrackCommand

  case class Stop(replyTo: ActorRef[Stopped])
    extends MiddleTrackCommand
    with OnlyTrackCommand
    with FirstTrackCommand
    with LastTrackCommand
object PlayerReplies {

  sealed trait Reply {
    def state: Player

  sealed trait SkippedReply extends Reply
  case class SkippedFromFirst(
      state: Player, replyTo: ActorRef[MiddleTrackCommand]) extends SkippedReply
  case class Skipped(state: Player) extends SkippedReply
  case class SkippedToLastTrack(
      state: Player, replyTo: ActorRef[LastTrackCommand]) extends SkippedReply

  sealed trait SkippedBackReply extends Reply
  case class SkippedBackFromLastTrack(
      state: Player, replyTo: ActorRef[MiddleTrackCommand]) extends SkippedBackReply
  case class SkippedBack(state: Player) extends SkippedBackReply
  case class SkippedBackToFirst(
      state: Player, replyTo: ActorRef[FirstTrackCommand]) extends SkippedBackReply

  case class FirstTrackEnqueued(
      state: Player, replyTo: ActorRef[OnlyTrackCommand]) extends Reply
  case class SecondTrackEnqueued(
      state: Player, replyTo: ActorRef[FirstTrackCommand]) extends Reply
  sealed trait TrackEnqueuedReply extends Reply
  case class TrackEnqueuedAfterLast(
      state: Player, replyTo: ActorRef[MiddleTrackCommand]) extends TrackEnqueuedReply
  case class TrackEnqueued(state: Player) extends TrackEnqueuedReply

  case class Stopped(state: Player, replyTo: ActorRef[EmptyCommand]) extends Reply

How does this protocol actually enforce the order of the messages? Take a look at this code snippet using the protocol.

object ExampleMain extends App {

  implicit val timeout: Timeout = 3.seconds
  implicit val system: ActorSystem[EmptyCommand] = ActorSystem(PlayerBehaviorFactory.initial(), "example")
  val track = Track("Example")

  val future = for {
    FirstTrackEnqueued(_, only) <- system.ask[FirstTrackEnqueued](EnqueueFirstTrack(track, _))
    SecondTrackEnqueued(_, first) <- only.ask[SecondTrackEnqueued](EnqueueSecondTrack(track, _))
    SkippedToLastTrack(_, last) <- first.ask[SkippedReply](Skip)
    TrackEnqueuedAfterLast(_, middle) <- last.ask[TrackEnqueuedReply](EnqueueTrack(track, _))
    Stopped(_, _) <- middle.ask[Stopped](Stop)
  } yield ()

  future.onComplete(_ => system.terminate())

As the example shows, I can’t send an EnqueueSecondTrack message without sending an EnqueueFirstTrack message first, because the initial behavior is an ActorRef[EmptyCommand] which clearly doesn’t accept enqueueing a second track when there’s no track enqueued yet. This is the same pattern that is often used in object-oriented programming, i.e. one method call returns an object that is needed for the next expected method call. Sometimes referred to as the “step builder” pattern.

Now the behavior definition is pretty straightforward. It just reacts to the commands by updating the state (here state means the in-memory state, holding the actual queue data in an instance of a class called Player) and transitioning to the new behavior accordingly. Some replies to the client include a replyTo field, meaning that the state changed and the client should now use the new actor reference for further commands, while others do not include a new actor reference, meaning the client can continue using the same one. Here is the behavior for the FirstTrack state.

private class FirstTrackBehavior(state: Player, ctx: ActorContext[PlayerCommand]) {

  val receive: Behavior[PlayerCommand] = Behaviors.receiveMessagePartial {
    case Skip(replyTo) =>
      val newState = state.skip()
      if (newState.isLastTrack) {
        replyTo ! SkippedToLastTrack(newState, ctx.self)
      } else {
        replyTo ! SkippedFromFirst(newState, ctx.self)

    case EnqueueTrack(track, replyTo) =>
      val newState = state.enqueue(track)
      replyTo ! TrackEnqueued(newState)

    case Stop(replyTo) =>
      val newState = state.stop()
      replyTo ! Stopped(newState, ctx.self)

There are a couple of things to discuss about this code.

Firstly, the behavior type Behavior[PlayerCommand]. Does that mean that FirstTrackBehavior, which is meant to only handle FirstTrackCommand’s, is in reality accepting any command, including for example SkipBack? Well, technically yes, and that’s the reason for the receiveMessagePartial. But don’t despair. Remember that the client will only see the behavior type through the lenses of an ActorRef[FirstTrackCommand] included in the replyTo response that it got by sending an EnqueueFirstTrack command to a previous ActorRef[EmptyCommand].

And secondly, the ctx.self sent back in the response to the client. This is related to my previous point. From this point of view ctx.self is of type ActorRef[PlayerCommand]. But take for instance the replyTo field in SkippedToLastTrack, it’s of type ActorRef[LastTrackCommand]. This trick effectively narrows the view for the client regarding the commands that it will be able to send to the player.

Another approach to this behavior implementation, is having multiple behaviors actually limited to handle the valid commands for the state, and instead of transitioning the same actor to a different behavior, spawning new actors each time the behavior changes. I tried this out, but spawning and stopping actors for each state change seemed not only an overkill but also very counter-intuitive.

This all comes down to a limitation of the language, because one can’t have a variable whose type changes in time, or whose methods can be called a limited number of times. Imagine an ActorRef[FirstTrackCommand] that receives a message and changes to ActorRef[MiddleTrackCommand], wouldn’t that be cool? There is a line of research called Linear logic programming which would be able to support these kinds of tricks. I recommend watching this talk if you’re interested.


Using akka-typed I was able to define a protocol where the type and order of messages are declared in advance. However, this can probably get too complex quickly when more business rules are introduced. So, as usual, the tradeoffs between type safety and complexity must be analyzed before jumping into implementation.

The complete implementation of my pet project can be found here. In addition to the protocol and behavior implementations, there’s a player_interface package. The idea is that in a real-world scenario, I don’t know in compile-time if the commands are received in the correct order. For instance, I could expose this to a user from a UI, or through http REST endpoints. So this is just a layer that translates those commands into internal protocol commands, or returns the corresponding errors if their order is incorrect.

What started as taking an online course just to be updated with the latest on a framework I like, turned out to be a lot of food for thought. And, ultimately, a good exercise regarding the best software development practices like separation of concerns, functional programming, and the practical limits of verifying the correctness of our software in compile time

Originally published at https://medium.com/@quiquerodrguez/there-is-more-to-akka-typed-than-meets-the-eye-4b8f66ba45b1 on Jun 22, 2020.

Read more on Akka

Read also on the blog

Download e-book:

Scalac Case Study Book

Download now


Enrique Rodríguez

I'm a passionate software engineer with experience delivering quality back-end software for a variety of companies. As an enthusiastic learner; I enjoy researching different technologies and architectures, finding the right tool for the job, and putting it into practice. Most recently, I have been focusing on Scala with Akka technologies, Blockchain, and ES/CQRS architectures. In my free time, you can find me learning music, playing the piano, or going for a run at the beach.

Latest Blogposts

07.06.2024 / By  Arkadiusz Kaczyński

Single tenant vs multitenancy – choosing the optimal solution.

Choosing between single tenant and multitenancy

What is Tenancy? Tenancy, what truly is it for? There is often a business need that involves using ecosystems by multiple organisations/clients and each of them wants their data to be separate from each other. You can achieve this with tenancy. You can do it with either single tenant deployment (setup per organisation) or with […]

06.06.2024 / By  Michał Talaśka

Java outsourcing projects: how to ensure security and compliance.

Java Outsourcing Development

In today’s world, security and compliance are paramount. A day without news of a data breach is quite rare. When it comes to outsourcing Java projects – one of our specialties – safety should be a priority. With the growing complexity and sophistication of cyber threats, businesses need to make sure that their Java outsourcing […]

30.05.2024 / By  Matylda Kamińska

Scalendar June 2024

Scalendar Scala conferences 2024

Event-driven Newsletter Welcome to June Scalendar! Join us in exploring conferences, meetups, and gatherings that promise to enrich your knowledge, expand your professional network, and inspire your career path. From Tokyo to Atlanta, Vienna to Rome, experts and enthusiasts from the global tech community come together to share knowledge, experiences and – last but not […]

software product development

Need a successful project?

Estimate project