Skip to main content

Durable state

DurableStateBehavior persists the actor's full current state as a single snapshot on every write, with no event history retained. Recovery loads the latest snapshot and the actor is immediately ready — no replay loop.

The design

The durable state model trades auditability for simplicity. There are no events, no event handlers, no sequence numbers to track. The command handler returns a DurableEffect that either persists the new state or replies without persisting.

src/Aggregates/UserProfileActor.php
<?php

declare(strict_types=1);

use Monadial\Nexus\Core\Actor\ActorContext;
use Monadial\Nexus\Persistence\PersistenceId;
use Monadial\Nexus\Persistence\State\DurableEffect;
use Monadial\Nexus\Persistence\State\DurableStateBehavior;

$behavior = DurableStateBehavior::create(
PersistenceId::of('UserProfile', $userId),
new UserProfile(),
static fn (ActorContext $ctx, object $cmd, UserProfile $state): DurableEffect => match (true) {
$cmd instanceof UpdateEmail => DurableEffect::persist($state->withEmail($cmd->email)),
$cmd instanceof UpdateName => DurableEffect::persist($state->withName($cmd->name)),
$cmd instanceof GetProfile => DurableEffect::none()->thenReply($cmd->replyTo, fn($s) => $s),
default => DurableEffect::unhandled(),
},
)
->withStateStore($stateStore)
->toBehavior();

DurableEffect

The command handler returns a DurableEffect:

  • DurableEffect::persist($newState) — write the entire new state object to the store.
  • DurableEffect::none() — acknowledge the command without writing.
  • DurableEffect::reply($to, $message) — send a reply as the sole effect.
  • DurableEffect::stash() — buffer the command; replay after $ctx->unstashAll().
  • DurableEffect::stop() — stop the actor.
  • DurableEffect::unhandled() — route to dead letters.

Chain ->thenReply($to, fn($state) => $msg) or ->thenRun(fn($state) => ...) to attach side-effects that execute after the state is durably stored.

Comparison with event sourcing

PropertyDurable stateEvent sourcing
What is storedLatest state snapshotFull event history
Recovery speedFast — load one recordSlower — replay all events
Audit trailNoneFull; every change is an event
Storage growthBounded — one row per entityUnbounded without retention policy
Read-model projectionNot possiblePossible — replay into any shape
Snapshot requiredImplicit (every write)Optional, accelerates recovery
ReplayFilter / writer conflictSame writerId stampingSame writerId stamping
Best forUser preferences, settings, cachesOrders, payments, ledgers, anything that needs history

When to use durable state

Use DurableStateBehavior when:

  • The actor represents mutable configuration or preferences with no need for history.
  • You want faster recovery and lower storage overhead than event sourcing provides.
  • The domain does not require audit trails or temporal queries.

Use EventSourcedBehavior when the command history matters — for example, financial transactions, order processing, or anything audited by compliance requirements.

DurableStateStore

DurableStateBehavior requires a DurableStateStore. Three implementations are available:

src/Bootstrap/PersistenceSetup.php
<?php

declare(strict_types=1);

use Monadial\Nexus\Persistence\Dbal\DbalDurableStateStore;

$store = new DbalDurableStateStore($connection);

$behavior = DurableStateBehavior::create($persistenceId, new UserProfile(), $commandHandler)
->withStateStore($store)
->toBehavior();

See also