Temporal — Interfaces#

Interface catalog#

MutableState#

  • Package: go.temporal.io/server/service/history/interfaces
  • File: service/history/interfaces/mutable_state.go
  • Methods: 80+ methods covering the full workflow execution lifecycle — AddActivityTaskScheduledEvent, AddWorkflowTaskStartedEvent, AddContinueAsNewEvent, GetWorkflowStateStatus, UpdateActivity, DeleteActivity, SetWorkflowTaskInfo, GetPendingActivityInfos, IsWorkflowExecutionRunning, and many more
  • Purpose: Central in-memory contract for a single workflow execution’s mutable state. Encapsulates every lifecycle event, pending task, activity, timer, child workflow, signal, and state transition for a running workflow. Also implements hsm.NodeBackend — bridging the old model to the new HSM framework.
  • Implementations: workflow.MutableStateImpl (the only real implementation; a mock is generated via mockgen)
  • Design quality: This is a deliberate “god interface” and the team knows it. At 394 lines with 80+ methods, it violates ISP significantly. The entire architectural migration to HSM/CHASM exists precisely to break this interface up into focused component interfaces. Newly added features (callbacks, nexus operations) are being built outside this interface, in the HSM/CHASM layer.

ExecutionManager#

  • Package: go.temporal.io/server/common/persistence
  • File: common/persistence/data_interfaces.go
  • Methods: CreateWorkflowExecution, UpdateWorkflowExecution, ConflictResolveWorkflowExecution, DeleteWorkflowExecution, GetWorkflowExecution, SetWorkflowExecution, ListConcreteExecutions, AddHistoryTasks, GetHistoryTasks, CompleteHistoryTask, AppendHistoryNodes, ReadHistoryBranch, ForkHistoryBranch, DeleteHistoryBranch, GetAllHistoryTreeBranches, and more (25+ total)
  • Purpose: The primary persistence contract for workflow execution data. Manages the workflow execution record (mutable state), the append-only history event tree, and the task queues (transfer, timer, replication) all in a single interface. All operations target a specific shard.
  • Implementations: cassandra.executionStore, sql.executionStore (for each SQL dialect), wrapped by executionManagerImpl which adds serialization/deserialization on top of the raw store.
  • Design quality: Large but purposeful — it represents a coherent storage boundary. The interface is defined by the consumer (History service) and implemented by each backend. Each method maps directly to one atomic database operation. The breadth reflects the reality that workflow execution data has many facets (state, events, tasks) that must be operated on together transactionally.

StateMachineDefinition#

  • Package: go.temporal.io/server/service/history/hsm
  • File: service/history/hsm/tree.go
  • Methods:
    • Type() string — identifies the state machine type
    • Serialize(any) ([]byte, error) — serializes state machine data to bytes
    • Deserialize([]byte) (any, error) — deserializes bytes back to state machine data
    • CompareState(any, any) (int, error) — determines which of two states is newer (for cross-cluster sync)
  • Purpose: Registration contract for plugging a new state machine type into the HSM registry. Any component that wants to live as a child node in the workflow’s hierarchical state machine tree must implement this interface and register with hsm.Registry.
  • Implementations: workflow.stateMachineDefinition (root workflow SM), callbacks.StateMachineDefinition, nexusoperations.MachineDefinition, and test implementations in components/
  • Design quality: Small, focused, follows ISP. The CompareState method has a TODO comment acknowledging it’s a temporary hook until transition history is fully implemented — honest technical debt management.

NodeBackend#

  • Package: go.temporal.io/server/service/history/hsm
  • File: service/history/hsm/tree.go
  • Methods:
    • AddHistoryEvent(t enumspb.EventType, setAttributes func(*historypb.HistoryEvent)) *historypb.HistoryEvent
    • LoadHistoryEvent(ctx context.Context, token []byte) (*historypb.HistoryEvent, error)
    • GetCurrentVersion() int64
    • NextTransitionCount() int64
  • Purpose: The seam between the HSM tree and the underlying workflow mutable state. The HSM framework calls through this interface when a state machine needs to append or read history events. This allows child state machines (callbacks, nexus operations) to emit history events without directly depending on the full MutableState interface.
  • Implementations: workflow.MutableStateImpl is the only implementation — it satisfies both MutableState and NodeBackend.
  • Design quality: Well-segregated. Only the four methods that HSM actually needs from mutable state are exposed. This is the architectural boundary between the old and new execution models.

hsm.Environment#

  • Package: go.temporal.io/server/service/history/hsm
  • File: service/history/hsm/executor.go
  • Methods:
    • Now() time.Time — wall clock backed by the shard’s time source
    • Access(ctx context.Context, ref Ref, accessType AccessType, accessor func(*Node) error) error — loads and locks the state machine node identified by ref, then calls accessor
  • Purpose: The execution context injected into every HSM task executor. Provides controlled access to state machine nodes (with proper locking/loading via the shard controller) and a shard-consistent time source. The AccessType enum (Read/Write) allows the framework to apply different locking strategies.
  • Implementations: Implemented by shard.contextImpl in the History service, which has access to the shard’s workflow cache and lock manager.
  • Design quality: Minimal — exactly two capabilities, both necessary. The Access pattern (pass an accessor closure rather than returning the node) is idiomatic Go and prevents accidental lock escapes.

chasm.Engine#

  • Package: go.temporal.io/server/chasm
  • File: chasm/engine.go
  • Methods:
    • StartExecution(ctx, ComponentRef, factory func(MutableContext) (RootComponent, error), ...TransitionOption) (StartExecutionResult, error)
    • UpdateWithStartExecution(ctx, ComponentRef, startFn, updateFn, ...TransitionOption) (EngineUpdateWithStartExecutionResult, error)
    • UpdateComponent(ctx, ComponentRef, func(MutableContext, Component) error, ...TransitionOption) ([]byte, error)
    • ReadComponent(ctx, ComponentRef, func(Context, Component) error, ...TransitionOption) error
    • PollComponent(ctx, ComponentRef, func(Context, Component) (bool, error), ...TransitionOption) ([]byte, error)
    • DeleteExecution(ctx, ComponentRef, DeleteExecutionRequest) error
    • NotifyExecution(ExecutionKey)
  • Purpose: The top-level CRUD API for the CHASM execution framework. Represents the entire contract between the request handlers (HTTP/gRPC) and the CHASM execution layer. All state transitions go through this interface. The engine is injected into request contexts via NewEngineContext, enabling handler code to invoke it without a direct dependency on the concrete implementation.
  • Implementations: engineImpl in chasm/ (in-progress). A mock is generated via //go:generate mockgen.
  • Design quality: Well-designed. The closure-based access pattern (func(MutableContext, Component) error) ensures components are only accessed while the engine holds a lock, preventing data races. The TransitionOption functional option pattern allows the API to evolve without breaking callers. The PollComponent method’s monotonic predicate contract is clearly documented.

chasm.Component / RootComponent / TerminableComponent#

  • Package: go.temporal.io/server/chasm
  • File: chasm/component.go
  • Methods (Component):
    • LifecycleState(Context) LifecycleState
    • mustEmbedUnimplementedComponent() — unexported, forces use of UnimplementedComponent embed
  • Methods (TerminableComponent extends Component):
    • Terminate(MutableContext, TerminateComponentRequest) (TerminateComponentResponse, error)
  • Methods (RootComponent extends TerminableComponent): (no additional methods in current version)
  • Purpose: Component is the base interface every CHASM state machine must implement. TerminableComponent adds forced-termination support. RootComponent marks the top-level component of an execution — when its lifecycle state closes, the entire execution is cleaned up. The unexported mustEmbed method forces implementors to embed UnimplementedComponent, providing forward compatibility as the interface grows.
  • Implementations: User-defined component structs embedding UnimplementedComponent. In the stdlib layer: chasm/lib/activity, chasm/lib/workflow, chasm/lib/scheduler each define their root components.
  • Design quality: The forced-embed pattern for forward compatibility is unusual in Go but explicit. The mustEmbedUnimplementedComponent() idiom, borrowed from gRPC’s Unsafe*Server pattern, prevents direct implementation without the embed, ensuring callers won’t break when the interface grows.

chasm.Context / MutableContext#

  • Package: go.temporal.io/server/chasm
  • File: chasm/context.go
  • Methods (Context):
    • Ref(Component) ([]byte, error)
    • Now(Component) time.Time
    • ExecutionKey() ExecutionKey
    • StateTransitionCount() int64
    • ExecutionCloseTime() time.Time
    • Logger() log.Logger
    • MetricsHandler() metrics.Handler
    • Value(key any) any
    • (plus unexported methods: withValue, structuredRef, goContext)
  • Methods (MutableContext extends Context):
    • AddTask(Component, TaskAttributes, any) — schedules a durable task for the component
  • Purpose: Read-only vs. read-write split of the component execution context. Context is passed to read-only handlers and observers; MutableContext is passed to update functions. This prevents accidental state mutation in read paths at compile time.
  • Implementations: immutableCtx (for Context), mutableCtx (for MutableContext) — both private to the chasm package.
  • Design quality: Excellent segregation. The observer/mutator split is idiomatic and prevents a common class of bugs. The Value(key any) any method mirrors context.Context intentionally, allowing framework-managed key-value injection without coupling component code to the framework internals.

dynamicconfig.Client#

  • Package: go.temporal.io/server/common/dynamicconfig
  • File: common/dynamicconfig/client.go
  • Methods:
    • GetValue(key Key) []ConstrainedValue
  • Purpose: The extension point for dynamic (runtime) configuration. A single-method interface that returns constrained values for a given key. The constrained values carry both the value and a Constraints struct (namespace, task queue, shard ID, etc.) that the Collection layer uses to select the most specific value for a given call context. This interface is the primary extensibility seam — operators implementing custom dynamic config backends (etcd, consul, launchdarkly, etc.) only need to implement this one method.
  • Implementations: fileBasedClient (polls a YAML file), noopClient (returns nothing, uses server defaults), memoryClient (for testing). An optional NotifyingClient interface (with Subscribe) can additionally be implemented for push-based change delivery.
  • Design quality: ISP exemplar. One method, one concern. The performance note in the comment (“called very often — don’t call external systems synchronously”) is practical and necessary.

membership.Monitor / ServiceResolver#

  • Package: go.temporal.io/server/common/membership
  • File: common/membership/interfaces.go
  • Methods (Monitor):
    • Start(), EvictSelf() error, EvictSelfAt(asOf time.Time) (time.Duration, error)
    • GetResolver(service primitives.ServiceName) (ServiceResolver, error)
    • GetReachableMembers() ([]string, error)
    • WaitUntilInitialized(context.Context) error
    • SetDraining(draining bool) error
    • ApproximateMaxPropagationTime() time.Duration
  • Methods (ServiceResolver):
    • Lookup(key string) (HostInfo, error), LookupN(key string, n int) []HostInfo
    • AddListener(name string, notifyChannel chan<- *ChangedEvent) error, RemoveListener(name string) error
    • MemberCount() int, AvailableMemberCount() int
    • Members() []HostInfo, AvailableMembers() []HostInfo
    • RequestRefresh()
  • Purpose: Monitor is the cluster-level membership view (join/leave/evict); ServiceResolver is the per-service hash-ring used to route requests to specific nodes (which History node owns shard 42?). The Lookup(key) method is how the Frontend determines which History node to send a workflow execution request to — key is typically namespaceID/workflowID and the ring maps it to a node address.
  • Implementations: ringpop.Monitor / ringpop.serviceResolver (gossip-based, production), static.Monitor / static.serviceResolver (config-based, dev/test)
  • Design quality: Clean two-interface split. The channel-based listener pattern for membership changes is idiomatic Go. AvailableMemberCount vs. MemberCount correctly distinguishes draining nodes from healthy ones.

authorization.Authorizer / ClaimMapper#

  • Package: go.temporal.io/server/common/authorization
  • Files: common/authorization/authorizer.go, common/authorization/claim_mapper.go
  • Methods (Authorizer):
    • Authorize(ctx context.Context, caller *Claims, target *CallTarget) (Result, error)
  • Methods (ClaimMapper):
    • GetClaims(authInfo *AuthInfo) (*Claims, error)
  • Purpose: Two-stage auth pipeline injected into the Frontend gRPC interceptor. ClaimMapper converts raw authentication info (JWT token, TLS certificate) into Temporal Claims (namespace roles, system role). Authorizer takes the resolved claims and a CallTarget (API name + namespace) and returns Allow/Deny. Separating these allows operators to customize either stage independently.
  • Implementations: noopAuthorizer (allow all), defaultAuthorizer (RBAC based on claims). noopClaimMapper (admin to all), defaultJWTClaimMapper (parses JWT + assigns roles). Both interfaces also have mockgen mocks.
  • Design quality: Excellent single-responsibility split. Each interface has exactly one method. The optional ClaimMapperWithAuthInfoRequired companion interface (also 1 method: AuthInfoRequired() bool) extends behavior without polluting the base interface — a good use of optional interface extension.

Interface patterns#

Size distribution#

  • 1–2 methods (ISP-compliant): The majority — dynamicconfig.Client (1), Authorizer (1), ClaimMapper (1), hsm.Environment (2), chasm.Component (2 effective), NodeBackend (4), StateMachineDefinition (4)
  • Medium (7–15 methods): chasm.Engine (7), membership.ServiceResolver (9), membership.Monitor (8)
  • Large (25+): ExecutionManager (25+), TaskManager (12), MutableState (80+)

The large interfaces (ExecutionManager, MutableState) exist at domain boundaries where the breadth is justified by the scope of the abstraction. MutableState’s breadth is recognized as a design problem being actively addressed.

Embedding#

  • Interface composition is used in the persistence layer: ShardManager, ExecutionManager, TaskManager all embed Closeable.
  • The chasm component hierarchy uses embedding: TerminableComponent embeds Component; RootComponent embeds TerminableComponent.
  • MutableContext embeds Context to extend the read-only context with mutation capabilities.

Implicit satisfaction#

  • Interfaces are overwhelmingly defined by consumers (the History service defines NodeBackend, which MutableState satisfies; HSM defines Environment, which shard.contextImpl satisfies). This is the Go idiom of interface ownership by the importer.
  • Authorization interfaces (Authorizer, ClaimMapper) are defined in a shared authorization package intended for operators to implement — provider-owned interfaces for the plugin pattern.
  • //go:generate mockgen annotations on all major interface files signal intent for dependency injection and testability.

Stdlib interfaces used#

  • No direct io.Reader/io.Writer use in domain interfaces — data is protobuf-serialized to []byte.
  • context.Context is pervasive as the first parameter in all persistence and engine interfaces.
  • chasm.Context.Value(key any) any deliberately mirrors context.Context.Value to make the chasm context feel ergonomically familiar.

Key abstractions#

  1. interfaces.MutableState — The most architecturally significant interface because it is both the heart of the current system and the thing being replaced. Its 80+ methods expose every aspect of a running workflow execution. Understanding why it is so large — and why the HSM/CHASM layers exist — is the single most important insight into Temporal’s architecture.

  2. persistence.ExecutionManager — The storage contract that makes multi-backend support possible. All of Cassandra, PostgreSQL, MySQL, and SQLite exist as swappable implementations of this interface. Its atomicity guarantees (events + mutable state + tasks in one write) are the consistency foundation of durable execution.

  3. chasm.Engine — The entry point into the new execution model. Its closure-based CRUD API (StartExecution, UpdateComponent, ReadComponent, PollComponent) is a cleaner, more type-safe version of the patterns buried inside the History service today. How widely this interface is adopted over the next few years will determine the architectural future of the project.

  4. hsm.StateMachineDefinition + NodeBackend — Together these two small interfaces are the migration seam. StateMachineDefinition is how new features plug into the tree framework; NodeBackend is how the tree framework reaches back into the legacy mutable state. They represent the architectural joint between old and new.

  5. dynamicconfig.Client — The simplest interface and arguably the most practically impactful for operators. A single GetValue call is all that separates the default file-based config polling from a real-time integration with any config service. Its minimalism is a deliberate design choice to reduce the barrier for custom implementations.


Interface-driven extensibility#

Temporal exposes four primary extension points via interfaces:

  1. Persistence backends (ExecutionManager, ShardManager, TaskManager, MetadataManager, VisibilityManager): Operators can implement new storage backends by satisfying these interfaces. In practice, Cassandra and SQL are the only production backends, but the interface structure would permit others.

  2. Authorization (Authorizer, ClaimMapper, TokenKeyProvider): The entire auth pipeline is pluggable. Operators running Temporal in enterprise environments supply custom implementations that integrate with their identity providers. These interfaces are documented with @@@SNIPSTART markers, indicating they are intended for external documentation.

  3. Dynamic configuration (dynamicconfig.Client, optionally NotifyingClient): Custom config backends (etcd, consul, launchdarkly, etc.) only need to implement the single-method Client interface and optionally the NotifyingClient for push-based delivery.

  4. HSM components (StateMachineDefinition): Features are added to Temporal by registering new state machine types with the hsm.Registry. The components/ directory shows the pattern: callbacks and nexusoperations each register their own state machine definitions, executors, and event definitions without touching the core MutableState code.

The CHASM Component / Engine layer represents a fifth extension point still under active development — one that is intended to eventually replace the persistence and HSM layers for new features entirely.