Choosing a Persistence Strategy for Stateful Akka Actors: A Decision Guide
Decide between no persistence, EventSourcedBehavior, and DurableStateBehavior for Akka actors based on auditability, recovery cost, and storage constraints, with a testkit-based validation path.
29 Apr 2026, 13:40 UTC

The decision: does state need to survive restart?
If an actor can rebuild its state from an external database or API on startup, adding a persistence journal is unnecessary overhead. Reserve Akka Persistence for state that is expensive or impossible to reconstruct, or when you need an immutable audit trail of how the state was reached.
Constraints that drive the choice
Three constraints usually decide the pattern:
- Auditability: Do you need to prove how state changed over time, or only what the current value is?
- Recovery cost: How expensive is replaying history versus loading a single snapshot?
- Storage and concurrency: Are you willing to accept write amplification and event-schema evolution, or do you prefer one overwrite per entity?
Supported options compared
| Option | Auditability | Recovery cost | Storage pattern | Concurrency |
|---|---|---|---|---|
| No persistence | None (external source only) | Low if external read is cheap | None in actor | Unconstrained |
EventSourcedBehavior | Full event history | Replay events; snapshots reduce cost | Append-only events per persistenceId | Per persistenceId |
DurableStateBehavior | Current state only | Load one state object | Overwrite single record per persistenceId | Per persistenceId |
Trade-offs in practice
No persistence
A plain Behavior with an explicit recovery path is the cheapest option. On start or passivation, the actor queries the external source and rebuilds itself. This avoids journal configuration, serialization adapters, and schema evolution entirely. The risk is that recovery becomes a hidden dependency: if the external source is unavailable, the actor cannot start.
EventSourcedBehavior
This model appends immutable domain events and rebuilds state by replaying them. It preserves the audit trail and supports snapshots to bound recovery time. Events can carry tags consumed by Projections to build read models. The cost is write amplification—every state change is a new event—and the burden of event-schema evolution. Renaming events or changing serialized fields can break replay unless you maintain adapters and compatibility tests.
DurableStateBehavior
This model persists the current state object as one logical record per persistenceId. It simplifies modeling and speeds recovery for large aggregates because there is no replay. However, it loses event history, offers coarser concurrency (one writer per persistenceId), and overwrites rather than appends. It suits entities where the current state is the truth and history is unnecessary.
Cluster Sharding and persistence IDs
Under Cluster Sharding, each entity needs a stable persistenceId. Derive it from the EntityTypeKey name and the entityId, for example with PersistenceId.of(entityTypeKey.name, entityId, separator). Passivation should flush state before the actor stops. Recovery must be idempotent because sharding can recreate an entity on a different node after a rebalance.
Concrete validation with Persistence TestKit
The typed persistence testkits let you assert behavior without a database. The exact package and method names vary across Akka 2.6, 2.7, and 2.8+, so treat the following as a structural template and verify signatures against your version.
// Run in: src/test/scala (sbt test)
// Replace testkit imports and factory methods with the exact names for your Akka version.
import akka.actor.typed.Behavior
import akka.persistence.testkit.{EventSourcedBehaviorTestKit, DurableStateBehaviorTestKit}
// ... other imports per your version
// 1. Event-sourced validation
val esTestKit = EventSourcedBehaviorTestKit[MyCommand, MyEvent, MyState](
persistenceId = "my-entity-1",
behavior = MyEventSourcedBehavior.create("my-entity-1")
)
esTestKit.runCommand(MyCommand.Change("new-value"))
// assert persisted effect, e.g.:
// val persisted = esTestKit.expectEffectPersisted(...)
// 2. Durable state validation
val dsTestKit = DurableStateBehaviorTestKit[MyCommand, MyState](
persistenceId = "my-entity-2",
behavior = MyDurableStateBehavior.create("my-entity-2")
)
dsTestKit.runCommand(MyCommand.Change("new-value"))
// assert persisted state, e.g.:
// val state = dsTestKit.expectStatePersisted(...)
These assertions confirm that commands produce the expected persisted events or state. They do not prove serialization or database schema correctness. Follow with one integration test against your chosen journal and snapshot-store plugin.
Configuration and integration checks
Both persistence styles require a journal and snapshot-store plugin declared in reference.conf. The plugin coordinates and keys differ by backend (R2DBC/Postgres, JDBC, Cassandra, etc.), so inspect the plugin's own reference.conf for required settings.
# application.conf (run with your application or test container)
akka.persistence {
journal.plugin = "my-journal-plugin"
snapshot-store.plugin = "my-snapshot-plugin"
}
my-journal-plugin {
class = "com.example.MyJournalPlugin" # verify exact class per plugin docs
# connection and schema keys per plugin
}
my-snapshot-plugin {
class = "com.example.MySnapshotPlugin" # verify exact class per plugin docs
}
Run the integration test with sbt "testOnly *PersistenceIntegrationSpec" from the project root. Expected checks: the actor recovers the same state after a system restart, snapshots are written and loaded, and event tags (if used) appear in the projection query. Risks include schema mismatches, serialization incompatibilities, and connection pool exhaustion under load.
Limitations and current verification
- Licensing: Recent Akka releases moved to BSL. Apache Pekko is the Apache-2.0 fork. Confirm the license and support terms for your exact version before standardizing.
- Version sensitivity: Testkit packages,
PersistenceIdconstruction, and Entity initialization APIs differ across Akka 2.6, 2.7, and 2.8+. Verify method names and module coordinates against the exact version in use. - Schema evolution: Event sourcing requires a compatibility strategy. Renaming or removing events, or changing serialized fields, can break replay.
- Sharding replay cost: A cluster restart can trigger many replays. Use snapshots and staggered passivation, and size journal and snapshot stores accordingly.
Practical result check
After implementing, verify the outcome by running the testkit assertions and the single integration test. If the actor recovers identical state after restart and the journal/snapshot plugin writes without schema errors, the persistence choice is validated for that entity. If recovery is slow or storage grows unexpectedly, revisit whether snapshots or a different persistence model are needed.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.