Migrating to Akka Typed Actors: A Practical Step‑by‑Step Guide
Learn how to migrate Akka actors to the Typed API for compile‑time message safety, with a concrete counter example, migration steps, and trade‑offs.
11 Jan 2026, 12:18 UTC

The problem: untyped messages slip past the compiler
In a classic Akka system you define messages as plain case classes and send them with actorRef ! msg. If you typo a message name or pass the wrong payload, the compiler stays silent and the error only surfaces at runtime as an unhandled message or a dead letter. This makes refactoring risky and slows down feedback loops.
Thesis: Akka Typed gives you compile‑time safety without sacrificing the actor model
Akka Typed replaces the untyped ActorRef with a typed reference that carries the protocol of messages the actor can receive. The compiler checks every send against that protocol, so mismatched messages are caught at build time. The underlying semantics—mailbox, lifecycle, supervision—remain unchanged, and performance impact is negligible.
Migration strategy: wrap, then replace
- Add the Typed dependency – for sbt:
(Use the matchinglibraryDependencies += "com.typesafe.akka" %% "akka-actor-typed" % "2.8.0"akka-actor-testkit-typedfor tests.) - Define a protocol as a sealed trait with case objects/classes for commands and replies.
sealed trait CounterProtocol final case class Increment(value: Int) extends CounterProtocol final case class GetValue(replyTo: ActorRef[Int]) extends CounterProtocol final case class CurrentValue(value: Int) extends CounterProtocol - Implement the actor behavior using the
Behavior[T]factory.object Counter { def apply(): Behavior[CounterProtocol] = { def updated(count: Int): Behavior[CounterProtocol] = Behaviors.receiveMessage { case Increment(v) => updated(count + v) case GetValue(replyTo) => replyTo ! CurrentValue(count) Behaviors.same } updated(0) } } - Create a typed reference and use it in your code.
val system: ActorSystem[CounterProtocol] = ActorSystem(Counter(), "counterSystem") val counter: ActorRef[CounterProtocol] = system counter ! Increment(5) counter ! GetValue(replyTo = system) // in practice you’d use a test probe or another actor - Gradually replace untyped actors by wrapping them in a typed façade or migrating one subsystem at a time. Keep the untyped
ActorSystemalive until all children are typed.
Worked example: a simple counter with test verification
Below is a minimal test using Akka Typed TestKit that asserts the counter’s behavior without starting a full ActorSystem.
import akka.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit
import org.scalatest.wordspec.AnyWordSpecLike
class CounterSpec extends ScalaTestWithActorTestKit with AnyWordSpecLike {
"A Counter actor" should {
"increment and return the current value" in {
val probe = createTestProbe[Int]()
val counter = spawn(Counter())
counter ! Increment(3)
counter ! GetValue(replyTo = probe.ref)
val reply = probe.receiveMessage()
assert(reply == 3)
}
}
}
Run the test with sbt test. If you change GetValue to send a String instead of an Int, the compiler will flag the mismatch at the line where you build the message, preventing a runtime surprise.
Trade‑offs and limitations
- Learning curve – developers must internalize the distinction between commands, replies, and events, and think in terms of
Behaviorfactories rather thanreceivemethods. - Library compatibility – some Akka modules (e.g., Akka Persistence Typed, Akka Streams Typed) have separate APIs and may require version‑specific dependencies. Mixing Typed and untyped code in the same project can increase build complexity.
- Reflection‑free guarantee – while the Typed layer avoids reflective dispatch for message handling, advanced features like
askpatterns still rely on runtime type information; ensure you test those paths.
Actionable closing
Start small: add the Typed dependency, define a protocol for one existing actor, and replace its untyped reference with a typed one. Use the testkit to verify behavior early. As confidence grows, migrate more actors and eventually enable Typed‑only modules like Persistence Typed. The compile‑time safety you gain reduces debugging time and makes refactoring safer, while the performance overhead remains practically nil.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.