Deterministic coordination stitches together version markers and replay-safe side effects. Use these primitives when evolving long-running workflows without duplicating work on retries or replays.
- Components
- One-shot execution
- Workflow builder
- Capturing deterministic steps
- Error behaviour
- Observability
Best practice: Persist deterministic state in durable storage (SQL, Cosmos DB, Redis) before performing out-of-process side effects so replays can resume from the same step without re-executing external calls.
VersionGaterecords an immutable version decision per change identifier using optimistic inserts (IDeterministicStateStore.TryAdd). Concurrent writers that lose the CAS receiveerror.version.conflictmetadata so callers can retry or fallback deterministically.DeterministicEffectStorecaptures idempotent side effects keyed by change/version/scope.DeterministicGatecombines both to execute code paths safely across replays.
The simplest overload mirrors the original upgraded/legacy split:
var outcome = await gate.ExecuteAsync(
changeId: "change.v2",
minVersion: 1,
maxVersion: 3,
upgraded: ct => processor.RunV3Async(ct),
legacy: ct => processor.RunV1Async(ct),
initialVersionProvider: _ => 2,
cancellationToken: ct);DeterministicGate persists the max version path by default. Subsequent executions reuse the original result by replaying the captured effect via DeterministicEffectStore.
DeterministicGate.Workflow<TResult> produces a branch builder for richer coordination. Configure predicates in declaration order and optionally supply a fallback:
var workflow = gate.Workflow<int>("customer.migration", 1, 3, _ => 2)
.For(decision => decision.IsNew, (ctx, ct) => ctx.CaptureAsync("init", _ => Task.FromResult(Result.Ok(0)), ct))
.ForVersion(2, async (ctx, ct) =>
{
await ctx.CaptureAsync("upgrade-db", token => migrator.RunAsync(ctx.Version, token), ct);
return Result.Ok(42);
})
.WithFallback((ctx, ct) => ctx.CaptureAsync("noop", () => Result.Ok(-1), ct));
var result = await workflow.ExecuteAsync(ct);ForVersion(version, ...)targets an exact version.ForRange(minVersion, maxVersion, ...)targets an inclusive range.For(predicate, ...)runs when the predicate matches the currentVersionDecision.WithFallback(...)executes when no branch qualifies.
Branches and the fallback always execute inside the deterministic effect envelope supplied by DeterministicEffectStore, so a replay reuses the stored result.
Inside a branch the provided DeterministicWorkflowContext exposes:
ctx.Version,ctx.IsNew, andctx.RecordedAtdescribing the decision.ctx.CreateEffectId("step")for manual identifiers.ctx.CaptureAsync(stepId, effect, cancellationToken)helpers that scope deterministic side effects underchangeId::v{version}::{stepId}.
Example step capture:
var response = await ctx.CaptureAsync(
stepId: "notify",
effect: token => notificationClient.SendAsync(payload, token),
cancellationToken: ct);If the effect already completed successfully during an earlier replay, CaptureAsync bypasses the delegate and returns the persisted Result<T> along with any metadata.
- Missing branches or unsupported versions surface
error.version.conflictwith metadata containingchangeId,version,minVersion, andmaxVersion. - Exceptions thrown inside a branch are converted to
error.exceptionviaError.FromException. OperationCanceledExceptionbecomeserror.canceled, preserving the triggering token when available.
Use these codes to build observability dashboards or to drive automated replay diagnostics.
- Instrument
GoDiagnosticsto emitworkflow.*metrics and activity tags. Replay counts flow through theworkflow.replay.counthistogram while logical clock increments surface underworkflow.logical.clock. DeterministicGateandDeterministicEffectStoreemitDeterministic.*activities (for exampleversion_gate.require,workflow.execute,effect.capture) so OmniRelay/SHOW HISTORY views can correlate commits with queue events.- When a deterministic branch fails, propagate
Result<T>.Error.Metadatainto logs or tracing scopes. Keys includechangeId,version,minVersion,maxVersion, and the scopedstepIdfromDeterministicWorkflowContext.CreateEffectId. - Attach
Result<T>.Error.Metadatato structured logs so OTLP/Prometheus pipelines can slice failures by change/version. For example, enrich Serilog scopes with@error.Metadataand configure OpenTelemetry resource attributes from the same payload. - Combine
DeterministicWorkflowContext.MetadatawithWorkflowExecutionContextto correlate deterministic steps with workflow executions. The latter already exports tags likeworkflow.namespace,workflow.id, andworkflow.logical_clockfor activity traces.