Skip to main content

ADR 0002: Immutable Behavior-Based Actor API

Status​

Accepted

Context​

Traditional actor frameworks (e.g., classic Akka) use mutable actor classes where state is stored in instance fields and mutated in message handlers. This leads to subtle bugs: handlers can accidentally share state references, and the actor's state is implicit rather than explicit.

Akka Typed introduced an alternative: immutable Behavior objects. Each message handler returns a new Behavior that defines how the next message should be processed. State transitions are explicit. The actor "becomes" a new behavior.

Decision​

Adopt the immutable behavior model:

  • Behavior<T> is a value object. It is never mutated.
  • Message handlers return the next Behavior (or Behavior::same() to keep current behavior).
  • State is captured in closures or passed explicitly via Behavior::withState().
  • Behavior::setup() provides access to ActorContext for spawning children, scheduling, and self-reference.
  • Special behaviors: Behavior::stopped(), Behavior::unhandled(), Behavior::same().

Two APIs for defining actors:

  1. Functional: Behavior::receive(fn) with closures — concise, good for simple actors.
  2. Class-based: ActorHandler with handle() — better for complex actors with dependencies.

Consequences​

  • No mutable actor state — all state changes produce a new behavior instance.
  • Behavior::withState($state, $handler) carries typed state through transitions.
  • Stashing is explicit: $ctx->stash() buffers messages, $ctx->unstashAll() replays them.
  • Props wraps behavior factories for spawning: Props::fromBehavior(), Props::fromFactory().
  • Testing is direct: behaviors are functions from (context, message) → behavior.