workflow

package
v0.9.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 23, 2026 License: Apache-2.0 Imports: 21 Imported by: 0

README

Workflows

See the workflow documentation.

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

View Source
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

View Source
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

func ByAggregateID[Data any](evt event.Of[Data]) (uuid.UUID, bool)

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

type Base struct {
	*aggregate.Base
	// contains filtered or unexported fields
}

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 New

func New(name string, id uuid.UUID) *Base

New returns a new workflow base for the workflow with the given aggregate name and id.

func (*Base) Done

func (b *Base) Done() bool

Done reports whether the workflow reached a terminal status (StatusCompleted, StatusCompensated, or StatusFailed).

func (*Base) MarshalSnapshot

func (b *Base) MarshalSnapshot() ([]byte, error)

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

func (b *Base) Reason() string

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) Status

func (b *Base) Status() Status

Status returns the lifecycle status of the workflow.

func (*Base) UnmarshalSnapshot

func (b *Base) UnmarshalSnapshot(p []byte) error

UnmarshalSnapshot implements snapshot.Unmarshaler. It restores the lifecycle and effect state of the workflow from a snapshot created by MarshalSnapshot.

type CommandDispatchedData

type CommandDispatchedData struct {
	EffectID uuid.UUID
}

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

type Correlator[Data any] func(event.Of[Data]) (uuid.UUID, bool)

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

func OnTimeout[W Workflow](
	key string,
	handler func(W, Ctx[TimeoutFiredData]) error,
) Option[W]

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

func (s *Service) Run(ctx context.Context) (<-chan error, error)

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.

func (*Service) Trigger

func (s *Service) Trigger(ctx context.Context, evt event.Event) error

Trigger processes an event through the same trigger path that is used for events from the event bus. It is mainly useful in tests and for manually feeding events into the workflow runtime.

type StartedData

type StartedData struct{}

StartedData is the payload of the Started event.

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

type TimeoutCanceledData struct {
	EffectID uuid.UUID
	Key      string
}

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

type TimeoutRequestedData struct {
	EffectID  uuid.UUID
	Key       string
	TriggerID uuid.UUID
	At        time.Time
}

TimeoutRequestedData is the payload of the TimeoutRequested event.

type TriggerRecordedData

type TriggerRecordedData struct {
	TriggerID   uuid.UUID
	TriggerName string
}

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)}
}

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL