Documentation
¶
Overview ¶
Package workflow provides an event-driven, distributed workflow runtime, also known as a saga or process manager.
A workflow instance is an event-sourced aggregate. Domain events start and advance workflows, and handlers record durable effects — outgoing commands and timeouts — as events on the workflow instead of executing them inline. A background runtime executes recorded effects and recovers them from the event store after restarts, so a workflow never loses an effect to a crash.
Workflows are defined statically with Define and run by a Service:
def := workflow.Define(
NewOrderWorkflow,
workflow.Starts(workflow.ByAggregateID, (*OrderWorkflow).onPlaced, OrderPlaced),
workflow.Reacts(workflow.ByAggregateID, (*OrderWorkflow).onPayment, PaymentReceived),
workflow.OnTimeout("payment", (*OrderWorkflow).onPaymentTimeout),
)
svc := workflow.NewService(workflow.Config{
EventStore: store,
EventBus: eventBus,
CommandBus: commandBus,
Commands: registry,
}, def)
errs, err := svc.Run(ctx)
Index ¶
- Constants
- Variables
- func ByAggregateID[Data any](evt event.Of[Data]) (uuid.UUID, bool)
- func RegisterEvents(r codec.Registerer)
- type Base
- type CommandDispatchedData
- type CommandRequestedData
- type CompensationCompletedData
- type CompensationFailedData
- type CompensationStartedData
- type CompletedData
- type Config
- type Correlator
- type Ctx
- type Definition
- type FailedData
- type Option
- func Compensates[W Workflow, Data any](correlate Correlator[Data], handler func(W, Ctx[Data]) error, ...) Option[W]
- func OnCompensationTimeout[W Workflow](key string, handler func(W, Ctx[TimeoutFiredData]) error) Option[W]
- func OnTimeout[W Workflow](key string, handler func(W, Ctx[TimeoutFiredData]) error) Option[W]
- func Reacts[W Workflow, Data any](correlate Correlator[Data], handler func(W, Ctx[Data]) error, ...) Option[W]
- func Starts[W Workflow, Data any](correlate Correlator[Data], handler func(W, Ctx[Data]) error, ...) Option[W]
- func WithRepository[W Workflow](repo aggregate.TypedRepository[W]) Option[W]
- type Service
- type StartedData
- type Status
- type TimeoutCanceledData
- type TimeoutFiredData
- type TimeoutRequestedData
- type TriggerRecordedData
- type Workflow
Constants ¶
const ( // Started marks the creation of a workflow instance. Started = "goes.workflow.started" // Completed marks a successfully finished workflow. Completed = "goes.workflow.completed" // Failed marks a failed workflow. Failed = "goes.workflow.failed" // CompensationStarted marks a workflow entering compensation. CompensationStarted = "goes.workflow.compensation.started" // CompensationCompleted marks a successfully compensated workflow. CompensationCompleted = "goes.workflow.compensation.completed" // CompensationFailed marks a compensation that ended unsuccessfully. CompensationFailed = "goes.workflow.compensation.failed" // TriggerRecorded stores the ids of trigger events already handled by a // workflow, making trigger handling idempotent. TriggerRecorded = "goes.workflow.trigger.recorded" // CommandRequested records an outgoing command effect. CommandRequested = "goes.workflow.command.requested" // CommandDispatched marks an outgoing command effect as dispatched. CommandDispatched = "goes.workflow.command.dispatched" // TimeoutRequested records an active timeout. TimeoutRequested = "goes.workflow.timeout.requested" // TimeoutCanceled cancels an active timeout. TimeoutCanceled = "goes.workflow.timeout.canceled" // TimeoutFired records a fired timeout and triggers timeout handlers. TimeoutFired = "goes.workflow.timeout.fired" )
Built-in workflow events. These events are recorded on workflow instances by the runtime and by handler contexts, and drive the lifecycle and effect state of every workflow.
Variables ¶
var ( // ErrEmptyEffectKey is returned when an effect key is empty. ErrEmptyEffectKey = errors.New("empty effect key") // ErrEffectConflict is returned when a handler records an effect under a // key that is already recorded with a different command. ErrEffectConflict = errors.New("conflicting effect") // ErrInvalidTransition is returned when a lifecycle transition is not // allowed in the current status of the workflow. ErrInvalidTransition = errors.New("invalid workflow transition") // ErrNotCompensating is returned when a compensation-only transition is // used outside of compensation. ErrNotCompensating = errors.New("workflow is not compensating") )
Functions ¶
func ByAggregateID ¶
ByAggregateID correlates trigger events to the workflow that has the same id as the aggregate that emitted the event — one workflow instance per aggregate. Events without an aggregate are ignored.
ByAggregateID is passed uninstantiated; its type argument is inferred from the handler of the registration:
workflow.Starts(workflow.ByAggregateID, (*OrderWorkflow).onPlaced, OrderPlaced)
func RegisterEvents ¶
func RegisterEvents(r codec.Registerer)
RegisterEvents registers the built-in workflow events into a codec registry, so that codec-backed event stores and buses can encode and decode their payloads.
Types ¶
type Base ¶
Base is the embeddable base type for workflow instances. It maintains the lifecycle status of the workflow and the state of its recorded effects, all derived from the built-in workflow events.
func (*Base) Done ¶
Done reports whether the workflow reached a terminal status (StatusCompleted, StatusCompensated, or StatusFailed).
func (*Base) MarshalSnapshot ¶
MarshalSnapshot implements snapshot.Marshaler. It deterministically encodes the lifecycle and effect state of the workflow, so that snapshot-enabled repositories (see WithRepository) can fetch workflows without replaying their full event history.
Workflows that carry state of their own must implement MarshalSnapshot and UnmarshalSnapshot themselves and include the Base state:
func (w *OrderWorkflow) MarshalSnapshot() ([]byte, error) {
base, err := w.Base.MarshalSnapshot()
if err != nil {
return nil, err
}
return marshal(orderSnapshot{Base: base, Items: w.items})
}
func (w *OrderWorkflow) UnmarshalSnapshot(p []byte) error {
var snap orderSnapshot
if err := unmarshal(p, &snap); err != nil {
return err
}
w.items = snap.Items
return w.Base.UnmarshalSnapshot(snap.Base)
}
func (*Base) Reason ¶
Reason returns why the workflow left the happy path: the reason of the most recent Fail, Compensate, or CompensationFailed transition. It returns an empty string for running and completed workflows.
func (*Base) UnmarshalSnapshot ¶
UnmarshalSnapshot implements snapshot.Unmarshaler. It restores the lifecycle and effect state of the workflow from a snapshot created by MarshalSnapshot.
type CommandDispatchedData ¶
CommandDispatchedData is the payload of the CommandDispatched event.
type CommandRequestedData ¶
type CommandRequestedData struct {
EffectID uuid.UUID
Key string
TriggerID uuid.UUID
CommandID uuid.UUID
Name string
AggregateName string
AggregateID uuid.UUID
Payload []byte
}
CommandRequestedData is the payload of the CommandRequested event.
type CompensationCompletedData ¶
type CompensationCompletedData struct{}
CompensationCompletedData is the payload of the CompensationCompleted event.
type CompensationFailedData ¶
type CompensationFailedData struct {
Reason string
}
CompensationFailedData is the payload of the CompensationFailed event.
type CompensationStartedData ¶
type CompensationStartedData struct {
Reason string
}
CompensationStartedData is the payload of the CompensationStarted event.
type CompletedData ¶
type CompletedData struct{}
CompletedData is the payload of the Completed event.
type Config ¶
type Config struct {
// EventStore is the event store that persists workflow instances.
EventStore event.Store
// EventBus is the event bus that delivers trigger events. Required if
// any definition registers trigger events.
EventBus event.Bus
// CommandBus is the command bus that dispatches recorded command
// effects.
CommandBus command.Bus
// Commands encodes and decodes command payloads. If it implements
// codec.Registerer, the built-in workflow events are registered into it
// as well — pass the codec registry of the application here.
Commands codec.Encoding
// NewRepository constructs the aggregate repository that fetches and
// saves workflow instances (default: repository.New(EventStore)).
// Definitions that bring their own repository (see WithRepository) are
// unaffected. The constructed repository must persist to EventStore.
NewRepository func(event.Store) aggregate.Repository
// Strict makes the Service report an error when a trigger event
// correlates to an unknown workflow, instead of silently ignoring it.
Strict bool
// Workers is the number of concurrent trigger workers (default 1).
// Trigger processing is synchronized per workflow, so multiple workers
// are safe.
Workers int
// DispatchInterval is the interval at which pending commands are
// dispatched and retried (default 100ms).
DispatchInterval time.Duration
// TimerResolution is the resolution of the timeout timer (default 25ms).
TimerResolution time.Duration
// TriggerReplayWindow enables the replay of trigger events from the
// event store when the service starts: trigger events no older than the
// window are re-processed, which recovers workflows that missed events
// while no service was running — useful with non-durable event buses
// such as NATS Core. Zero (the default) disables the replay. Already
// handled triggers are deduplicated, so generous windows are safe.
TriggerReplayWindow time.Duration
// ResyncInterval is the interval at which the effect runtime re-reads
// effect events from the event store (default 1m; a negative duration
// disables periodic resyncs). Resyncs are the liveness backstop of the
// effect runtime: they discover effects that were recorded by other
// service instances — letting any instance take over the pending
// commands and timeouts of a crashed one — and recover local effect
// notifications that were dropped under load. Without periodic resyncs,
// effects recorded by other instances are only picked up at startup.
//
// Effects discovered through resyncs are attempted only once they are
// older than a short takeover grace, giving the instance that recorded
// them a head start and avoiding duplicate dispatches between healthy
// instances.
ResyncInterval time.Duration
// RecoveryWindow bounds how far the initial effect recovery reaches back
// into the event store when the service starts (default 30 days; a
// negative duration scans the full history). It keeps the startup scan
// proportional to recent activity instead of the total history of the
// store. Effects recorded before the window are not recovered after a
// restart, so the window must exceed the longest timeout horizon of the
// workflows plus the maximum expected downtime.
RecoveryWindow time.Duration
}
Config configures a Service.
type Correlator ¶
A Correlator finds the workflow instance that should handle a trigger event. It returns the id of the workflow and whether the event correlates to a workflow at all. A Correlator is just a function — write your own to correlate by whatever the event provides:
func byOrder(evt event.Of[PaymentReceivedData]) (uuid.UUID, bool) {
return evt.Data().OrderID, true
}
func ByKey ¶
func ByKey[Data any](namespace uuid.UUID, key func(Data) string) Correlator[Data]
ByKey returns a Correlator that derives the workflow id from a business key in the event payload, using deterministic (UUIDv5) derivation within the given namespace — one workflow instance per key. All events that yield the same key correlate to the same workflow, regardless of which aggregate emitted them. Events for which the key function returns an empty string are ignored.
var customers = uuid.MustParse("d3f0a1de-52a7-40e8-8a3a-79dbd8f2071e")
workflow.Starts(
workflow.ByKey(customers, func(d OrderPlacedData) string { return d.CustomerEmail }),
(*LoyaltyWorkflow).onOrder,
OrderPlaced,
)
type Ctx ¶
type Ctx[Data any] interface { context.Context // Event returns the trigger event. Event() event.Of[Data] // Dispatch records an outgoing command effect under the given key. The // runtime dispatches the command after the workflow was saved, and // re-dispatches it after restarts until the dispatch was recorded, making // command dispatch at-least-once. Recording the same key with the same // command twice is a no-op; recording it with a different command returns // ErrEffectConflict. Dispatch(key string, cmd command.Command) error // Schedule records a timeout with the given key that fires at the given // time. Scheduling a key that already has an active timeout replaces it. Schedule(key string, at time.Time) error // Unschedule cancels the active timeout with the given key. Canceling // a key without an active timeout is a no-op. Unschedule(key string) error // Complete transitions the workflow to StatusCompleted and cancels all // active timeouts. Complete() error // Fail transitions the workflow to StatusFailed and cancels all active // timeouts. Fail(reason error) error // Compensate transitions the workflow to StatusCompensating and cancels // all active timeouts. From here on, only Compensates and // OnCompensationTimeout handlers run. Compensate(reason error) error // Compensated transitions the compensating workflow to // StatusCompensated. Compensated() error // CompensationFailed transitions the compensating workflow to // StatusFailed. CompensationFailed(reason error) error }
Ctx is the context passed to workflow trigger handlers. It gives handlers access to the trigger event and records effects and lifecycle transitions as events on the workflow. Effects are not executed inline: they are persisted together with the other changes of the workflow and executed by the Service runtime.
type Definition ¶
type Definition struct {
// contains filtered or unexported fields
}
Definition is the static definition of a workflow type: its constructor, the trigger events that start and advance its instances, and optionally its own repository (see WithRepository). Definitions are created with Define and passed to NewService.
func Define ¶
func Define[W Workflow](create func(uuid.UUID) W, opts ...Option[W]) Definition
Define creates a workflow definition from a constructor and options:
def := workflow.Define(
NewOrderWorkflow,
workflow.Starts(workflow.ByAggregateID, (*OrderWorkflow).onPlaced, OrderPlaced),
workflow.Reacts(workflow.ByAggregateID, (*OrderWorkflow).onPayment, PaymentReceived),
workflow.OnTimeout("payment", (*OrderWorkflow).onPaymentDue),
// Optional: fetch and save instances through a custom (e.g.
// snapshot-enabled) repository instead of the Service default.
// See WithRepository for details.
workflow.WithRepository(repository.Typed(repo, NewOrderWorkflow)),
)
type FailedData ¶
type FailedData struct {
Reason string
}
FailedData is the payload of the Failed event.
type Option ¶
type Option[W Workflow] struct { // contains filtered or unexported fields }
An Option configures a workflow definition. Use Starts, Reacts, Compensates, OnTimeout, and OnCompensationTimeout to register trigger handlers, and WithRepository to configure the repository of the definition.
func Compensates ¶
func Compensates[W Workflow, Data any]( correlate Correlator[Data], handler func(W, Ctx[Data]) error, eventNames ...string, ) Option[W]
Compensates registers a handler for trigger events that advance a compensating workflow. Compensates handlers only run while the workflow has StatusCompensating.
func OnCompensationTimeout ¶
func OnCompensationTimeout[W Workflow]( key string, handler func(W, Ctx[TimeoutFiredData]) error, ) Option[W]
OnCompensationTimeout registers a handler for the timeout with the given key that runs when the timeout fires while the workflow is compensating.
func OnTimeout ¶
OnTimeout registers a handler for the timeout with the given key, scheduled via Ctx.Schedule. The handler runs when the timeout fires while the workflow is running.
func Reacts ¶
func Reacts[W Workflow, Data any]( correlate Correlator[Data], handler func(W, Ctx[Data]) error, eventNames ...string, ) Option[W]
Reacts registers a handler for trigger events that advance a running workflow. Events that correlate to an unknown workflow are ignored, unless the Service runs in strict mode.
func Starts ¶
func Starts[W Workflow, Data any]( correlate Correlator[Data], handler func(W, Ctx[Data]) error, eventNames ...string, ) Option[W]
Starts registers a handler for trigger events that may start a new workflow instance. If the correlated workflow already exists, the handler behaves like a Reacts handler.
func WithRepository ¶
func WithRepository[W Workflow](repo aggregate.TypedRepository[W]) Option[W]
WithRepository configures the repository through which the instances of the workflow definition are fetched and saved, replacing the default repository of the Service (see Config.NewRepository). Use it to enable snapshots or repository hooks for a workflow type:
repo := repository.New(store, repository.WithSnapshots(snapshots, snapshot.Every(10))) def := workflow.Define( NewOrderWorkflow, workflow.WithRepository(repository.Typed(repo, NewOrderWorkflow)), workflow.Starts(workflow.ByAggregateID, (*OrderWorkflow).onPlaced, OrderPlaced), )
The repository must persist to the same event store the Service is configured with: effect recovery and trigger replay read from Config.EventStore directly.
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
Service runs distributed workflow instances on top of an event bus and event store. Trigger events from the bus start and advance workflows; recorded effects (commands and timeouts) are executed by a background runtime that survives restarts by recovering from the event store.
The error channel returned by Run must be drained; an undrained channel eventually pauses the runtime.
func NewService ¶
func NewService(cfg Config, defs ...Definition) *Service
NewService returns a new workflow service that runs the given workflow definitions.
func (*Service) Run ¶
Run starts the workflow runtime. It subscribes to the registered trigger events, optionally replays recent triggers from the event store (Config.TriggerReplayWindow), and starts the effect runtime that dispatches commands and fires timeouts.
Callers must drain the returned error channel.
type Status ¶
type Status string
Status is the lifecycle status of a workflow instance.
const ( // StatusRunning is the status of a started workflow that has not reached // a terminal status yet. StatusRunning Status = "running" // StatusCompleted is the terminal status of a successfully finished // workflow. StatusCompleted Status = "completed" // StatusCompensating is the status of a workflow that is undoing its // previous work after a business failure. StatusCompensating Status = "compensating" // StatusCompensated is the terminal status of a workflow whose // compensation finished successfully. StatusCompensated Status = "compensated" // StatusFailed is the terminal status of a workflow that failed, either // directly or because its compensation failed. StatusFailed Status = "failed" )
type TimeoutCanceledData ¶
TimeoutCanceledData is the payload of the TimeoutCanceled event.
type TimeoutFiredData ¶
type TimeoutFiredData struct {
EffectID uuid.UUID
WorkflowID uuid.UUID
Key string
ScheduledFor time.Time
}
TimeoutFiredData is the payload of the TimeoutFired event.
type TimeoutRequestedData ¶
TimeoutRequestedData is the payload of the TimeoutRequested event.
type TriggerRecordedData ¶
TriggerRecordedData is the payload of the TriggerRecorded event.
type Workflow ¶
type Workflow interface {
aggregate.TypedAggregate
// contains filtered or unexported methods
}
Workflow is a single workflow instance. Workflows are event-sourced aggregates that embed *Base, which provides the lifecycle state of the workflow and satisfies this interface:
type OrderWorkflow struct {
*workflow.Base
}
func NewOrderWorkflow(id uuid.UUID) *OrderWorkflow {
return &OrderWorkflow{Base: workflow.New("shop.order_workflow", id)}
}