Style guide
This is a style guide with recommendations of idioms and patterns for writing Akka actors. Note that this guide does not cover the classic actor API.
As with all style guides, treat this as a list of rules to be broken. There are certainly times when alternative styles should be preferred over the ones given here.
Functional versus object-oriented style
There are two flavors of the Actor APIs.
- The functional programming style where you pass a function to a factory which then constructs a behavior, for stateful actors this means passing immutable state around as parameters and switching to a new behavior whenever you need to act on a changed state.
- The object-oriented style where a concrete class for the actor behavior is defined and mutable state is kept inside of it as fields.
An example of a counter actor implemented in the functional style:
- Scala
-
source
import akka.actor.typed.Behavior import akka.actor.typed.scaladsl.ActorContext import akka.actor.typed.scaladsl.Behaviors object Counter { sealed trait Command case object Increment extends Command final case class GetValue(replyTo: ActorRef[Value]) extends Command final case class Value(n: Int) def apply(): Behavior[Command] = counter(0) private def counter(n: Int): Behavior[Command] = Behaviors.receive { (context, message) => message match { case Increment => val newValue = n + 1 context.log.debug("Incremented counter to [{}]", newValue) counter(newValue) case GetValue(replyTo) => replyTo ! Value(n) Behaviors.same } } } - Java
Corresponding actor implemented in the object-oriented style:
- Scala
-
source
import akka.actor.typed.Behavior import akka.actor.typed.scaladsl.ActorContext import akka.actor.typed.scaladsl.Behaviors import akka.actor.typed.scaladsl.AbstractBehavior import org.slf4j.Logger object Counter { sealed trait Command case object Increment extends Command final case class GetValue(replyTo: ActorRef[Value]) extends Command final case class Value(n: Int) def apply(): Behavior[Command] = { Behaviors.setup(context => new Counter(context)) } } class Counter(context: ActorContext[Counter.Command]) extends AbstractBehavior[Counter.Command](context) { import Counter._ private var n = 0 override def onMessage(msg: Command): Behavior[Counter.Command] = { msg match { case Increment => n += 1 context.log.debug("Incremented counter to [{}]", n) this case GetValue(replyTo) => replyTo ! Value(n) this } } } - Java
Some similarities to note:
- Messages are defined in the same way.
- Both have an
apply
factory method in the companion object to create the initial behavior, i.e. from the outside they are used in the same way. - Pattern matching and handling of the messages are done in the same way.
- The
ActorContext
API is the same.
A few differences to note:
- There is no class in the functional style, but that is not strictly a requirement and sometimes it’s convenient to use a class also with the functional style to reduce number of parameters in the methods.
- Mutable state, such as the
var n
is typically used in the object-oriented style. - In the functional style the state is updated by returning a new behavior that holds the new immutable state, the
n: Int
parameter of thecounter
method. - The object-oriented style must use a new instance of the initial
Behavior
for each spawned actor instance, since the state inAbstractBehavior
instance must not be shared between actor instances. This is “hidden” in the functional style since the immutable state is captured by the function. - In the object-oriented style one can return
this
to stay with the same behavior for next message. In the functional style there is nothis
soBehaviors.same
is used instead. - The
ActorContext
is accessed in different ways. In the object-oriented style it’s retrieved fromBehaviors.setup
and kept as an instance field, while in the functional style it’s passed in alongside the message. That said,Behaviors.setup
is often used in the functional style as well, and then often together withBehaviors.receiveMessage
that doesn’t pass in the context with the message.
Which style you choose to use is a matter of taste and both styles can be mixed depending on which is best for a specific actor. An actor can switch between behaviors implemented in different styles. For example, it may have an initial behavior that is only stashing messages until some initial query has been completed and then switching over to its main active behavior that is maintaining some mutable state. Such initial behavior is nice in the functional style and the active behavior may be better with the object-oriented style.
We would recommend using the tool that is best for the job. The APIs are similar in many ways to make it easy to learn both. You may of course also decide to just stick to one style for consistency and familiarity reasons.
When developing in Scala the functional style will probably be the choice for many.
Some reasons why you may want to use the functional style:
- You are familiar with a functional approach of structuring the code. Note that this API is still not using any advanced functional programming or type theory constructs.
- The state is immutable and can be passed to “next” behavior.
- The
Behavior
is stateless. - The actor lifecycle has several different phases that can be represented by switching between different behaviors, like a finite state machine. This is also supported with the object-oriented style, but it’s typically nicer with the functional style.
- It’s less risk of accessing mutable state in the actor from other threads, like
Future
or Streams callbacks.
Some reasons why you may want to use the object-oriented style:
- You are more familiar with an object-oriented style of structuring the code with methods in a class rather than functions.
- Some state is not immutable.
- It could be more familiar and easier to upgrade existing classic actors to this style.
- Mutable state can sometimes have better performance, e.g. mutable collections and avoiding allocating new instance for next behavior (be sure to benchmark if this is your motivation).
Passing around too many parameters
One thing you will quickly run into when using the functional style is that you need to pass around many parameters.
Let’s add name
parameter and timers to the previous Counter
example. A first approach would be to just add those as separate parameters:
- Scala
-
source
// this is an anti-example, better solutions exists object Counter { sealed trait Command case object Increment extends Command final case class IncrementRepeatedly(interval: FiniteDuration) extends Command final case class GetValue(replyTo: ActorRef[Value]) extends Command final case class Value(n: Int) def apply(name: String): Behavior[Command] = Behaviors.withTimers { timers => counter(name, timers, 0) } private def counter(name: String, timers: TimerScheduler[Command], n: Int): Behavior[Command] = Behaviors.receive { (context, message) => message match { case IncrementRepeatedly(interval) => context.log.debug( "[{}] Starting repeated increments with interval [{}], current count is [{}]", name, interval.toString, n.toString) timers.startTimerWithFixedDelay(Increment, interval) Behaviors.same case Increment => val newValue = n + 1 context.log.debug("[{}] Incremented counter to [{}]", name, newValue) counter(name, timers, newValue) case GetValue(replyTo) => replyTo ! Value(n) Behaviors.same } } } - Java
Ouch, that doesn’t look good. More things may be needed, such as stashing or application specific “constructor” parameters. As you can imagine, that will be too much boilerplate.
As a first step we can place all these parameters in a class so that we at least only have to pass around one thing. Still good to have the “changing” state, the n: Int
here, as a separate parameter.
- Scala
-
source
// this is better than previous example, but even better solution exists object Counter { sealed trait Command case object Increment extends Command final case class IncrementRepeatedly(interval: FiniteDuration) extends Command final case class GetValue(replyTo: ActorRef[Value]) extends Command final case class Value(n: Int) private case class Setup(name: String, context: ActorContext[Command], timers: TimerScheduler[Command]) def apply(name: String): Behavior[Command] = Behaviors.setup { context => Behaviors.withTimers { timers => counter(Setup(name, context, timers), 0) } } private def counter(setup: Setup, n: Int): Behavior[Command] = Behaviors.receiveMessage { case IncrementRepeatedly(interval) => setup.context.log.debug( "[{}] Starting repeated increments with interval [{}], current count is [{}]", setup.name, interval, n) setup.timers.startTimerWithFixedDelay(Increment, interval) Behaviors.same case Increment => val newValue = n + 1 setup.context.log.debug("[{}] Incremented counter to [{}]", setup.name, newValue) counter(setup, newValue) case GetValue(replyTo) => replyTo ! Value(n) Behaviors.same } } - Java
That’s better. Only one thing to carry around and easy to add more things to it without rewriting everything. Note that we also placed the ActorContext
in the Setup
class, and therefore switched from Behaviors.receive
to Behaviors.receiveMessage
since we already have access to the context
.
It’s still rather annoying to have to pass the same thing around everywhere.
We can do better by introducing an enclosing class, even though it’s still using the functional style. The “constructor” parameters can be immutable instance fields and can be accessed from member methods.
- Scala
-
source
// this is better than previous examples object Counter { sealed trait Command case object Increment extends Command final case class IncrementRepeatedly(interval: FiniteDuration) extends Command final case class GetValue(replyTo: ActorRef[Value]) extends Command final case class Value(n: Int) def apply(name: String): Behavior[Command] = Behaviors.setup { context => Behaviors.withTimers { timers => new Counter(name, context, timers).counter(0) } } } class Counter private ( name: String, context: ActorContext[Counter.Command], timers: TimerScheduler[Counter.Command]) { import Counter._ private def counter(n: Int): Behavior[Command] = Behaviors.receiveMessage { case IncrementRepeatedly(interval) => context.log.debug( "[{}] Starting repeated increments with interval [{}], current count is [{}]", name, interval, n) timers.startTimerWithFixedDelay(Increment, interval) Behaviors.same case Increment => val newValue = n + 1 context.log.debug("[{}] Incremented counter to [{}]", name, newValue) counter(newValue) case GetValue(replyTo) => replyTo ! Value(n) Behaviors.same } } - Java
That’s nice. One thing to be cautious with here is that it’s important that you create a new instance for each spawned actor, since those parameters must not be shared between different actor instances. That comes natural when creating the instance from Behaviors.setup
as in the above example. Having a apply
factory method in the companion object and making the constructor private is recommended.
This can also be useful when testing the behavior by creating a test subclass that overrides certain methods in the class. The test would create the instance without the apply
factory method . Then you need to relax the visibility constraints of the constructor and methods.
It’s not recommended to place mutable state and var
members in the enclosing class. It would be correct from an actor thread-safety perspective as long as the same instance of the enclosing class is not shared between different actor instances, but if that is what you need you should rather use the object-oriented style with the AbstractBehavior
class.
Similar can be achieved without an enclosing class by placing the def counter
inside the Behaviors.setup
block. That works fine, but for more complex behaviors it can be better to structure the methods in a class. For completeness, here is how it would look like:
- Scala
-
source
// this works, but previous example is better for structuring more complex behaviors object Counter { sealed trait Command case object Increment extends Command final case class IncrementRepeatedly(interval: FiniteDuration) extends Command final case class GetValue(replyTo: ActorRef[Value]) extends Command final case class Value(n: Int) def apply(name: String): Behavior[Command] = Behaviors.setup { context => Behaviors.withTimers { timers => def counter(n: Int): Behavior[Command] = Behaviors.receiveMessage { case IncrementRepeatedly(interval) => context.log.debug( "[{}] Starting repeated increments with interval [{}], current count is [{}]", name, interval, n) timers.startTimerWithFixedDelay(Increment, interval) Behaviors.same case Increment => val newValue = n + 1 context.log.debug("[{}] Incremented counter to [{}]", name, newValue) counter(newValue) case GetValue(replyTo) => replyTo ! Value(n) Behaviors.same } counter(0) } } }
Behavior factory method
The initial behavior should be created via a factory method in the companion object . Thereby the usage of the behavior doesn’t change when the implementation is changed, for example if changing between object-oriented and function style.
The factory method is a good place for retrieving resources like Behaviors.withTimers
, Behaviors.withStash
and ActorContext
with Behaviors.setup
.
When using the object-oriented style, AbstractBehavior
, a new instance should be created from a Behaviors.setup
block in this factory method even though the ActorContext
is not needed. This is important because a new instance should be created when restart supervision is used. Typically, the ActorContext
is needed anyway.
The naming convention for the factory method is apply
(when using Scala) . Consistent naming makes it easier for readers of the code to find the “starting point” of the behavior.
In the functional style the factory could even have been defined as a val
if all state is immutable and captured by the function, but since most behaviors need some initialization parameters it is preferred to consistently use a method (def
) for the factory.
Example:
- Scala
-
source
object CountDown { sealed trait Command case object Down extends Command // factory for the initial `Behavior` def apply(countDownFrom: Int, notifyWhenZero: ActorRef[Done]): Behavior[Command] = new CountDown(notifyWhenZero).counter(countDownFrom) } private class CountDown(notifyWhenZero: ActorRef[Done]) { import CountDown._ private def counter(remaining: Int): Behavior[Command] = { Behaviors.receiveMessage { case Down => if (remaining == 1) { notifyWhenZero.tell(Done) Behaviors.stopped } else counter(remaining - 1) } } }
- Java
When spawning an actor from this initial behavior it looks like:
Where to define messages
When sending or receiving actor messages they should be prefixed with the name of the actor/behavior that defines them to avoid ambiguities.
Such a style is preferred over using importing Down
and using countDown ! Down
. However, within the Behavior
that handle these messages the short names can be used.
Therefore it is not recommended to define messages as top-level classes.
For the majority of cases it’s good style to define the messages in the companion object together with the Behavior
.
- Scala
-
source
object Counter { sealed trait Command case object Increment extends Command final case class GetValue(replyTo: ActorRef[Value]) extends Command final case class Value(n: Int) }
- Java
If several actors share the same message protocol, it’s recommended to define those messages in a separate object
for that protocol.
Here’s an example of a shared message protocol setup:
- Scala
-
source
object CounterProtocol { sealed trait Command final case class Increment(delta: Int, replyTo: ActorRef[OperationResult]) extends Command final case class Decrement(delta: Int, replyTo: ActorRef[OperationResult]) extends Command sealed trait OperationResult case object Confirmed extends OperationResult final case class Rejected(reason: String) extends OperationResult }
- Java
Note that the response message hierarchy in this case could be completely avoided by using the API instead (see Generic Response Wrapper).
Public versus private messages
Often an actor has some messages that are only for its internal implementation and not part of the public message protocol, such as timer messages or wrapper messages for ask
or messageAdapter
.
Such messages should be declared private
so they can’t be accessed and sent from the outside of the actor. Note that they must still extend the public Command
trait .
Here is an example of using private
for an internal message:
- Scala
-
source
object Counter { sealed trait Command case object Increment extends Command final case class GetValue(replyTo: ActorRef[Value]) extends Command final case class Value(n: Int) // Tick is private so can't be sent from the outside private case object Tick extends Command def apply(name: String, tickInterval: FiniteDuration): Behavior[Command] = Behaviors.setup { context => Behaviors.withTimers { timers => timers.startTimerWithFixedDelay(Tick, tickInterval) new Counter(name, context).counter(0) } } } class Counter private (name: String, context: ActorContext[Counter.Command]) { import Counter._ private def counter(n: Int): Behavior[Command] = Behaviors.receiveMessage { case Increment => val newValue = n + 1 context.log.debug("[{}] Incremented counter to [{}]", name, newValue) counter(newValue) case Tick => val newValue = n + 1 context.log.debug("[{}] Incremented counter by background tick to [{}]", name, newValue) counter(newValue) case GetValue(replyTo) => replyTo ! Value(n) Behaviors.same } }
- Java
An alternative approach is using a type hierarchy and narrow
to have a super-type for the public messages as a distinct type from the super-type of all actor messages. The former approach is recommended but it is good to know this alternative as it can be useful when using shared message protocol classes as described in Where to define messages.
Here’s an example of using a type hierarchy to separate public and private messages:
- Scala
-
source
// above example is preferred, but this is possible and not wrong object Counter { // The type of all public and private messages the Counter actor handles sealed trait Message /** Counter's public message protocol type. */ sealed trait Command extends Message case object Increment extends Command final case class GetValue(replyTo: ActorRef[Value]) extends Command final case class Value(n: Int) // The type of the Counter actor's internal messages. sealed trait PrivateCommand extends Message // Tick is a private command so can't be sent to an ActorRef[Command] case object Tick extends PrivateCommand def apply(name: String, tickInterval: FiniteDuration): Behavior[Command] = { Behaviors .setup[Counter.Message] { context => Behaviors.withTimers { timers => timers.startTimerWithFixedDelay(Tick, tickInterval) new Counter(name, context).counter(0) } } .narrow // note narrow here } } class Counter private (name: String, context: ActorContext[Counter.Message]) { import Counter._ private def counter(n: Int): Behavior[Message] = Behaviors.receiveMessage { case Increment => val newValue = n + 1 context.log.debug("[{}] Incremented counter to [{}]", name, newValue) counter(newValue) case Tick => val newValue = n + 1 context.log.debug("[{}] Incremented counter by background tick to [{}]", name, newValue) counter(newValue) case GetValue(replyTo) => replyTo ! Value(n) Behaviors.same } }
- Java
private
visibility can be defined for the PrivateCommand
messages but it’s not strictly needed since they can’t be sent to an ActorRef[Command] , which is the public message type of the actor.
Partial versus total Function
It’s recommended to use a sealed
trait as the super type of the commands (incoming messages) of an actor as the compiler will emit a warning if a message type is forgotten in the pattern match.
- Scala
-
source
sealed trait Command case object Down extends Command final case class GetValue(replyTo: ActorRef[Value]) extends Command final case class Value(n: Int)
That is the main reason for Behaviors.receive
, Behaviors.receiveMessage
taking a Function
rather than a PartialFunction
.
The compiler warning if GetValue
is not handled would be:
[warn] ... Counter.scala:45:34: match may not be exhaustive.
[warn] It would fail on the following input: GetValue(_)
[warn] Behaviors.receiveMessage {
[warn] ^
Note that a MatchError
will be thrown at runtime if a message is not handled, so it’s important to pay attention to those. If a Behavior
should not handle certain messages you can still include them in the pattern match and return Behaviors.unhandled
.
- Scala
-
source
val zero: Behavior[Command] = { Behaviors.receiveMessage { case GetValue(replyTo) => replyTo ! Value(0) Behaviors.same case Down => Behaviors.unhandled } }
It’s recommended to use the sealed
trait and total functions with exhaustiveness check to detect mistakes of forgetting to handle some messages. Sometimes that can be inconvenient and then you can use a PartialFunction
with Behaviors.receivePartial
or Behaviors.receiveMessagePartial
How to compose Partial Functions
Following up from previous section, there are times when one might want to combine different PartialFunction
s into one Behavior
.
A good use case for composing two or more PartialFunction
s is when there is a bit of behavior that repeats across different states of the Actor. Below, you can find a simplified example for this use case.
The Command definition is still highly recommended be kept within a sealed
Trait:
- Scala
-
source
sealed trait Command case object Down extends Command final case class GetValue(replyTo: ActorRef[Value]) extends Command final case class Value(n: Int)
In this particular case, the Behavior that is repeating over is the one in charge to handle the GetValue
Command, as it behaves the same regardless of the Actor’s internal state. Instead of defining the specific handlers as a Behavior
, we can define them as a PartialFunction
:
- Scala
-
source
def getHandler(value: Int): PartialFunction[Command, Behavior[Command]] = { case GetValue(replyTo) => replyTo ! Value(value) Behaviors.same } def setHandlerNotZero(value: Int): PartialFunction[Command, Behavior[Command]] = { case Down => if (value == 1) zero else nonZero(value - 1) } def setHandlerZero(log: Logger): PartialFunction[Command, Behavior[Command]] = { case Down => log.error("Counter is already at zero!") Behaviors.same }
Finally, we can go on defining the two different behaviors for this specific actor. For each Behavior
we would go and concatenate all needed PartialFunction
instances with orElse
to finally apply the command to the resulting one:
- Scala
-
source
val zero: Behavior[Command] = Behaviors.setup { context => Behaviors.receiveMessagePartial(getHandler(0).orElse(setHandlerZero(context.log))) } def nonZero(capacity: Int): Behavior[Command] = Behaviors.receiveMessagePartial(getHandler(capacity).orElse(setHandlerNotZero(capacity))) // Default Initial Behavior for this actor def apply(initialCapacity: Int): Behavior[Command] = nonZero(initialCapacity)
Even though in this particular example we could use receiveMessage
as we cover all cases, we use receiveMessagePartial
instead to cover potential future unhandled message cases.
ask versus ?
When using the AskPattern
it’s recommended to use the ask
method rather than the infix ?
operator, like so:
- Scala
-
source
import akka.actor.typed.scaladsl.AskPattern._ import akka.util.Timeout implicit val timeout: Timeout = Timeout(3.seconds) val counter: ActorRef[Command] = ??? val result: Future[OperationResult] = counter.ask(replyTo => Increment(delta = 2, replyTo))
You may also use the more terse placeholder syntax _
instead of replyTo
:
However, using the infix operator ?
with the placeholder syntax _
, like is done in the following example, won’t typecheck because of the binding scope rules for wildcard parameters:
- Scala
-
source
// doesn't compile val result3: Future[OperationResult] = counter ? Increment(delta = 2, _)
Adding the necessary parentheses (as shown below) makes it typecheck, but, subjectively, it’s rather ugly so the recommendation is to use ask
.
Note that AskPattern
is only intended for request-response interaction from outside an actor. If the requester is inside an actor, prefer ActorContext.ask
as it provides better thread-safety by not requiring the use of a Future
inside the actor.
Nesting setup
When an actor behavior needs more than one of setup
, withTimers
and withStash
the methods can be nested to access the needed dependencies:
- Scala
-
source
def apply(): Behavior[Command] = Behaviors.setup[Command](context => Behaviors.withStash(100)(stash => Behaviors.withTimers { timers => context.log.debug("Starting up") // behavior using context, stash and timers ... }))
- Java
The order of the nesting does not change the behavior as long as there is no additional logic in any other function than the innermost one. It can be nice to default to put setup
outermost as that is the least likely block that will be removed if the actor logic changes.
Note that adding supervise
to the mix is different as it will restart the behavior it wraps, but not the behavior around itself:
- Scala
-
source
def apply(): Behavior[Command] = Behaviors.setup { context => // only run on initial actor start, not on crash-restart context.log.info("Starting") Behaviors .supervise(Behaviors.withStash[Command](100) { stash => // every time the actor crashes and restarts a new stash is created (previous stash is lost) context.log.debug("Starting up with stash") // Behaviors.receiveMessage { ... } }) .onFailure[RuntimeException](SupervisorStrategy.restart) }
- Java
Additional naming conventions
Some naming conventions have already been mentioned in the context of other recommendations, but here is a list of additional conventions:
-
replyTo
is the typical name for theActorRef[Reply]
parameter in messages to which a reply or acknowledgement should be sent. -
Incoming messages to an actor are typically called commands, and therefore the super type of all messages that an actor can handle is typically
sealed trait Command
. -
Use past tense for the events persisted by an
EventSourcedBehavior
since those represent facts that have happened, for exampleIncremented
.