Using Akka Typed Actors to Eliminate Unhandled Messages
Learn how Akka Typed Actors enforce compile‑time message safety, see a counter example, and avoid common pitfalls like blocking calls or mixed APIs.
29 Jul 2025, 03:45 UTC

Why use Akka Typed Actors
Typed actors give each actor a single message type, so the compiler can verify that every possible message is handled. This removes the need for runtime pattern matches on untyped ActorRef and reduces the chance of unhandled messages.
Worked example: a simple counter
sealed trait CounterCmd
case object Increment extends CounterCmd
final case class GetValue(replyTo: ActorRef[Int]) extends CounterCmd
val counterBehavior: Behavior[CounterCmd] = Behaviors.setup { ctx =>
var count = 0
Behaviors.receiveMessage {
case Increment =>
count += 1
Behaviors.same
case GetValue(replyTo) =>
replyTo ! count
Behaviors.same
}
}
val system = ActorSystem(Behaviors.empty, "counter-demo")
val counter = system.spawn(counterBehavior, "counter")
counter ! Increment
counter ! GetValue(replyTo = system.ignoreRef) // replace with a test probe in real code
How the typed behavior works
The Behavior[Msg] defines a state transition function. Behaviors.setup creates the actor instance and provides a context for spawning children or accessing the system. Behaviors.receiveMessage processes each incoming message of type Msg and returns the next behavior, enabling immutable or mutable state updates.
Configuration considerations
Typed actors inherit the same dispatcher and mailbox settings as untyped actors. You can override them in application.conf:
akka.actor.default-dispatcher { type = Dispatcher executor = "thread-pool-executor" thread-pool-executor.fixed-pool-size = 16 } my-bounded-mailbox { mailbox-type = "akka.dispatch.BoundedMailbox" mailbox-capacity = 1000 mailbox-push-timeout-time = 10s }Assign the mailbox to an actor:
val counter = system.spawn(counterBehavior, "counter", Props.empty.withMailbox("my-bounded-mailbox"))Limits and common mistakes
- Blocking calls inside a Behavior – Using
Thread.sleepor synchronous I/O occupies dispatcher threads, reducing throughput and can cause deadlocks, especially in a cluster. Replace blocking work with theaskpattern or offload to a dedicated blocking dispatcher. - Mixing typed and untyped APIs – Passing an
UntypedActorRefto a typed actor or expecting an untypedaskto work with aBehaviorleads to dead letters or unhandled messages. Keep the API boundary consistent. - Missing message case – If a
Behaviordoes not handle a message type and is not defined withBehaviors.unhandled, the message is silently dropped. Enableakka.log-unhandledto detect this.
How to verify the example
- Create an sbt project with Akka 2.6.x or 2.7.x dependency.
- Place the code snippet in
src/main/scala/CounterDemo.scala. - Run
sbt run(requires read/execute permissions on the project directory). - Observe the console; the program should terminate without errors. To see the reply, replace
system.ignoreRefwith a test probe and print the received integer.
Practical check: enable akka.log-config-on-start = on and akka.log-dead-letters = 10 to see if any messages are dropped or sent to dead letters.
When to consider alternatives
If you need dynamic message handling or integration with legacy untyped code, the untyped API may be simpler, but you lose compile‑time safety. For high‑throughput, non‑blocking workloads, typed actors paired with Akka Streams provide back‑pressure and bounded mailboxes.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.