Migrating an existing codebase

Statesman is designed for staged adoption. The safe path is to establish observation and lineage first, then move ownership.

Phase 1: Inventory state

Identify state that is copied, cached, refreshed, invalidated, or independently mutated in several places. Give each concept one static StateKey<T>. Do not start by converting every DTO or local variable.

Phase 2: Mirror legacy updates

Declare state with no proactive loader and push values from existing change events, polling jobs, or setters.

[StateMutationBoundary("Legacy bridge until session ownership moves to Statesman.")]
public sealed class LegacySessionBridge
{
    public ValueTask PublishAsync(LegacySession legacy) =>
        _state.SetAsync(
            new SessionState(legacy.UserId, legacy.IsAuthenticated),
            new StateWriteOptions { Source = "legacy-session" });
}

Consumers can begin reading IState<T> while the legacy system remains the source. This immediately creates snapshots, observation, history, and diagnostics.

Phase 3: Replace duplicate caches

Move existing acquisition code into declared loaders. Keep the old API surface as an adapter over IState<T> so callers do not all change at once.

Phase 4: Move transitions

Replace ad hoc mutation with UpdateAsync or typed interactions. Prefer immutable records. Add invariants at the state boundary.

Phase 5: Turn on analyzers

Install Statesman.Analyzers as warnings first. Mark authoritative state types with [ManagedState]. Add explicit mutation boundaries only around intentional adapters. Ratchet STM001 and STM003 to errors in CI once the migration surface is understood.

[*.cs]
dotnet_diagnostic.STM001.severity = error
dotnet_diagnostic.STM002.severity = warning
dotnet_diagnostic.STM003.severity = error

Phase 6: Move durability

Start in memory. Select filesystem storage for local applications, Redis for a shared low-latency authority, EF Core for relational durability, or a tiered store when hot and cold responsibilities differ.

Upgrading across the serializer-envelope release

ROADMAP 0.3 Phase 22 adds StateRecord.Envelope and StateCommit.Envelope, stamped on every new write with the content type and serializer id the runtime wrote with. Nothing in an existing deployment needs to change to pick this up. Records written before this release read back with a null envelope, and no stored byte moves to get there. A consumer on Entity Framework Core who uses one of the shipped migration packages applies one generated SerializerEnvelope migration per engine (docs/providers/entity-framework-core-migrations.md); a consumer who owns their own migrations must add one nullable EnvelopeJson column to each of StatesmanLedgerHead and StatesmanLedgerRecord in a migration of their own before upgrading — the column being nullable means there is no backfill, not that there is nothing to apply. Without it, every read fails with a missing-column error. Nothing validates an envelope on read this release, so an envelope that disagrees with the serializer actually in use changes no behaviour today — that check is not built yet.

Avoid the dual-authority trap

A mirror is intentionally temporary. Document which side is authoritative, tag mirrored writes with a source, and give the bridge an exit condition. Two components that both write independently are not migration, they are competing state authorities.