Status: Draft 1.3
Target: Go 1.27+
Persistence: Local transactional database such as SQLite or bbolt
Schema and code generation: Protocol Buffers, Buf, and protoc-gen-durable
| File | Covers |
|---|---|
| 01-model.md | Core identities, resource slots, Runs, scheduling, failure taxonomy, Outcome and Result |
| 02-authoring.md | Pipeline and Step declarations, handlers, the State API, Reducers, the application-facing API, worked examples |
| 03-evolution.md | Execution ledger, forward and unwind frontiers, pinning, reordering, retirement, removal, unwind semantics, breaking changes |
| 04-engine.md | Engine lifecycle, recovery, retries, attempts, invalid Runs, concurrency, shutdown, observability |
| 05-codegen.md | Protobuf options, protoc-gen-durable outputs, generation-time validation, Buf |
| invariants.md | The core invariants checklist |
| future-work.md | Deferred design areas |
| http-analogy.md | Non-normative design note: the net/http analogy behind middleware and func adapters |
durable is a Go library for executing linear pipelines whose execution state survives process crashes and restarts.
A pipeline consists of an ordered sequence of Steps:
A -> B -> C -> D
Each Step operation uses at-least-once execution semantics. Step handlers therefore MUST be idempotent.
Successful forward execution automatically advances to the next applicable Step.
An ordinary error means the current operation remains unresolved and MUST be retried.
A handler explicitly declares permanent operation failure with:
return durable.Fail(err)During forward execution, permanent failure establishes a run failure and begins unwind.
For:
A ✓ -> B ✓ -> C ✓ -> D X
unwind normally proceeds:
C <- B <- A
subject to the current Pipeline definition and the Run's monotonic unwind frontier.
Pipeline definitions may evolve while Runs are active. durable does not persist a complete immutable Pipeline topology per Run. Instead, it persists execution facts and reconciles them against the currently registered Pipeline definition.
A Step may be marked retired as the intermediate stage before removal. Retirement prevents new Runs from entering that Step while allowing already-started operations to resolve.
Pipeline Output is optionally produced by a pure Reducer over immutable Pipeline Input and committed Step States.
durable intentionally keeps the authoring model small.
Correctness responsibilities are divided into three layers:
Code generation
structural API correctness
Engine configuration
current Pipeline/Step registration correctness
Per-Run reconciliation
compatibility between persisted execution facts
and the current Pipeline definition
If an existing nonterminal Run cannot be reconciled safely against the current application definition, the Engine does not attempt increasingly complex migration semantics.
Instead:
Run -> invalid for current deployment
The Engine:
- logs a diagnostic,
- exposes the condition through status and future observability,
- ignores the Run for execution,
- continues processing other valid Runs.
A corrected future deployment MAY make the Run valid and runnable again.
Invalidity is an operational runtime condition, not a Pipeline business failure.
The module is one package per audience plus one shared leaf, and every import arrow points from the more specific package to the more general one:
kernel identities, phases, outcomes, parks, failure records
^ ^ ^
durable store/driver observe
^ (store SPI) (lifecycle events)
pipelinedef ^
^ store Open(uri) + driver registry
engine ^
^ store/bbolt, store/mem, store/sqlite (later)
contrib/durableotel, generated code, applications
durableis the handler contract and nothing else:Invocation,Failure,Fail, theAwait*resolutions,HandlerandMiddlewarewith their classifiers, the step references, and aliases of thekernelvocabulary. Because handlers schedule child Runs, the contract also holds what that call needs: theScheduleOptions,ScheduleConflictError,ErrRunNotFound, andErrRunTerminal. Handler code imports only this.engineis the wiring side. Its exported signatures use thedurablealiases, so aStatus.RunIDreads asdurable.RunID.pipelinedefis what generated code builds andEngine.Bindvalidates. Application code does not import it.storeopens a store from a URI through drivers that register their scheme ininit, so a binary links only the drivers it blank-imports;store/driveris the SPI they implement.store/memis a real driver for ephemeral runs, not a test double.kernelhas no audience of its own; it exists so thatstore/driverandobservedo not depend on the handler contract, and the handler contract does not depend on the store SPI.
Normative identifiers in this specification are qualified by package
where it matters. Unqualified handler-facing names (Invocation,
Fail, Wake) are the durable package's; unqualified runtime names
(Engine, Run, Result, Status, RunState) are engine's.
The model is:
linear current topology
+
typed immutable Input
+
typed immutable Step State
+
durable execution ledger
+
monotonic forward frontier
+
monotonic unwind frontier
+
Step retirement
+
automatic retry
+
explicit permanent failure
+
reverse unwind
+
pure Output reduction
+
crash recovery
Initial non-goals include:
- distributed execution,
- distributed consensus,
- remote workers,
- distributed queues,
- parallel branches,
- DAG execution,
- loops,
- child workflows,
- deterministic program replay,
- exactly-once external side effects,
- arbitrary workflow migration languages,
- BPMN,
- Petri nets,
- distributed transactions.
The durable runtime model is:
current Pipeline definition
+
Run execution ledger
|
v
reconcile execution
|
+-------------+-------------+
| |
forward unwind
| |
monotonic frontier monotonic frontier
| |
v v
Step.Run Step.Unwind
Forward topology evolution:
insert after frontier
-> may execute
insert before frontier
-> skipped for that Run
unresolved Step
-> pins Run until resolution
retired + never started
-> bypass
retired + already started
-> finish normally
removed while still required
-> Run invalid
Unwind topology evolution:
successful forward
+
currently present
+
currently unwindable
+
ahead of unwind frontier
-> eligible
removed
-> not eligible
never executed
-> not eligible
becomes eligible behind frontier
-> skipped
State access:
state, ok := inv.State(machines.SomeStep)and:
state, ok := pipeline.State(machines.SomeStep)Reducer:
immutable Input
+
immutable committed Step States
|
v
Reducer
|
v
immutable Pipeline Output
Failure semantics:
ordinary error
-> retry
durable.Fail during Run
-> Failure
-> unwind
durable.Fail during Unwind
-> permanent Failure
-> continue backward
runtime incompatibility
-> RunStateInvalid
-> diagnose
-> ignore until repaired
The central persistence principle is:
Persist immutable execution facts, then interpret them against the currently registered linear Pipeline definition using monotonic execution frontiers.
The central compatibility principle is:
Normal compatible evolution is reconciled automatically. Anything that cannot be reconciled safely is an observable runtime-invalid Run rather than a reason to complicate the workflow model or stop the entire Engine.