Choosing Between EventSourcedBehavior and DurableStateBehavior in Akka Persistence Typed
Guidance on picking EventSourcedBehavior or DurableStateBehavior in Akka Persistence Typed, with trade‑offs, a compact comparison table, and ready‑to‑run Scala snippets.
25 Jul 2026, 20:25 UTC

Decision: EventSourcedBehavior vs DurableStateBehavior
When designing an entity with Akka Persistence Typed you must choose how state is stored. The choice influences storage cost, recovery time, and the ability to build read‑side projections or audit trails.
Constraints and assumptions
- Akka version 2.7+ (BSL) or 2.6.x (Apache 2.0) – verify license compatibility.
- Entity will be placed under Cluster Sharding.
- Journal and snapshot store are pluggable (JDBC, Cassandra, etc.).
- Downstream consumers may need either the full event stream or only the latest state.
Comparison table
| Aspect | EventSourcedBehavior | DurableStateBehavior |
|---|---|---|
| What is persisted | Journal of events; state derived by replay | Only the latest state snapshot per persistenceId |
| Recovery process | Replay all events (bounded by snapshots) | Load latest snapshot directly |
| Storage footprint | Grows with number of events | Constant (one snapshot per entity) |
| Audit / history | Full event log available | No history unless you add external logging |
| Read‑side integration | Can feed Akka Projections, eventsByTag, etc. | No native event stream; would need polling or snapshots |
| Schema evolution | Requires upcasting or versioned events | State class changes must be backward compatible |
| Typical use case | Domains needing audit, temporal queries, CQRS, event‑driven services | Simple CRUD‑like entities where only current value matters |
Trade‑offs
If you need an immutable audit trail or want to build multiple read models from the same event stream, EventSourcedBehavior is the only built‑in way to achieve that without extra components. The downside is higher storage consumption and the operational discipline of evolving event schemas.
When the entity behaves like a mutable record and no downstream consumer depends on the event log, DurableStateBehavior reduces both storage and recovery latency. You lose the ability to replay history and to feed tag‑based projections directly; switching later would require a migration of existing snapshots to an event journal.
Concrete implementation
The following Scala snippets show a minimal entity for each style. Replace MyCommand, MyEvent and MyState with your domain types.
EventSourcedBehavior example
import akka.persistence.typed.scaladsl.EventSourcedBehavior
import akka.persistence.typed.PersistenceId
object MyEntity {
sealed trait Command
case class AddItem(item: String, replyTo: akka.actor.typed.ActorRef[Confirmation]) extends Command
case object GetState extends Command
sealed trait Event
case class ItemAdded(item: String) extends Event
final case class State(items: List[String] = Nil) {
def updated(evt: Event): State = evt match {
case ItemAdded(item) => State(item :: items)
}
}
val behavior = EventSourcedBehavior[Command, Event, State](
persistenceId = PersistenceId.ofUniqueId("my-entity"),
emptyState = State(),
commandHandler = (state, cmd) => cmd match {
case AddItem(item, replyTo) =>
Effect.persist(ItemAdded(item)).thenRun(_ => replyTo ! Confirmation(item))
case GetState => Effect.reply(state)
},
eventHandler = (state, evt) => state.updated(evt)
)
}
DurableStateBehavior example
import akka.persistence.typed.scaladsl.DurableStateBehavior
import akka.persistence.typed.PersistenceId
object MyEntity {
sealed trait Command
case class UpdateItem(item: String, replyTo: akka.actor.typed.ActorRef[Confirmation]) extends Command
case object GetItem extends Command
final case class State(item: Option[String] = None)
val behavior = DurableStateBehavior[Command, State](
persistenceId = PersistenceId.ofUniqueId("my-entity"),
emptyState = State(),
commandHandler = (state, cmd) => cmd match {
case UpdateItem(item, replyTo) =>
val newState = State(Some(item))
Effect.persist(newState).thenRun(_ => replyTo ! Confirmation(item))
case GetItem => Effect.reply(state.item)
}
)
}
Both snippets assume you have configured an Akka persistence plugin (e.g., akka.persistence.journal.plugin = "akka.persistence.journal.jdbc") and a snapshot store if you want to bound recovery.
Validation approach
- Unit test with testkit – Use
EventSourcedBehaviorTestKitandDurableStateBehaviorTestKitto verify command handling and that recovery from an empty journal yields the expected state. This does not require a running database. - License check – Inspect your build (
sbt akkaVersionor Mavendependency:tree) to confirm the Akka version and verify that the BSL terms are acceptable for your deployment. - Recovery benchmark – In a staging environment, create an entity with a realistic number of events (e.g., 10 000) and measure the time to recover with and without snapshots. Compare the results; a significant difference indicates that snapshots are needed for EventSourcedBehavior.
- Journal query verification – If your read side relies on
eventsByTagor persistence‑id queries, run an integration test against the actual journal plugin (e.g., Cassandra) and assert that the query returns the expected events. For DurableStateBehavior, confirm that no such query is needed.
Limitations and practical checks
- EventSourcedBehavior requires you to treat events as version‑stable contracts; a breaking change will need an upcaster or migration strategy.
- DurableStateBehavior cannot be used as a source for Akka Projections; if you later need a projection you must migrate data to an event journal.
- Both styles depend on the underlying journal plugin’s capabilities; some plugins (e.g., in‑memory) lack persistent queries, which would break a read‑side that expects them.
- Practical check: after deploying, monitor the journal table size growth rate and the average recovery latency reported by Akka’s
akka.persistence.query.current.persistenceIdsmetric. If growth exceeds your storage budget or recovery latency spikes, consider switching the persistence style or adjusting snapshot policy.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.