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 byexecutionManagerImplwhich 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 typeSerialize(any) ([]byte, error)— serializes state machine data to bytesDeserialize([]byte) (any, error)— deserializes bytes back to state machine dataCompareState(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 incomponents/ - Design quality: Small, focused, follows ISP. The
CompareStatemethod 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.HistoryEventLoadHistoryEvent(ctx context.Context, token []byte) (*historypb.HistoryEvent, error)GetCurrentVersion() int64NextTransitionCount() 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
MutableStateinterface. - Implementations:
workflow.MutableStateImplis the only implementation — it satisfies bothMutableStateandNodeBackend. - 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 sourceAccess(ctx context.Context, ref Ref, accessType AccessType, accessor func(*Node) error) error— loads and locks the state machine node identified byref, then callsaccessor
- 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
AccessTypeenum (Read/Write) allows the framework to apply different locking strategies. - Implementations: Implemented by
shard.contextImplin the History service, which has access to the shard’s workflow cache and lock manager. - Design quality: Minimal — exactly two capabilities, both necessary. The
Accesspattern (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) errorPollComponent(ctx, ComponentRef, func(Context, Component) (bool, error), ...TransitionOption) ([]byte, error)DeleteExecution(ctx, ComponentRef, DeleteExecutionRequest) errorNotifyExecution(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:
engineImplinchasm/(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. TheTransitionOptionfunctional option pattern allows the API to evolve without breaking callers. ThePollComponentmethod’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) LifecycleStatemustEmbedUnimplementedComponent()— unexported, forces use ofUnimplementedComponentembed
- Methods (TerminableComponent extends Component):
Terminate(MutableContext, TerminateComponentRequest) (TerminateComponentResponse, error)
- Methods (RootComponent extends TerminableComponent): (no additional methods in current version)
- Purpose:
Componentis the base interface every CHASM state machine must implement.TerminableComponentadds forced-termination support.RootComponentmarks the top-level component of an execution — when its lifecycle state closes, the entire execution is cleaned up. The unexportedmustEmbedmethod forces implementors to embedUnimplementedComponent, 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/schedulereach 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’sUnsafe*Serverpattern, 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.TimeExecutionKey() ExecutionKeyStateTransitionCount() int64ExecutionCloseTime() time.TimeLogger() log.LoggerMetricsHandler() metrics.HandlerValue(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.
Contextis passed to read-only handlers and observers;MutableContextis 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 thechasmpackage. - Design quality: Excellent segregation. The observer/mutator split is idiomatic and prevents a common class of bugs. The
Value(key any) anymethod mirrorscontext.Contextintentionally, 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
Constraintsstruct (namespace, task queue, shard ID, etc.) that theCollectionlayer 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 optionalNotifyingClientinterface (withSubscribe) 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) errorSetDraining(draining bool) errorApproximateMaxPropagationTime() time.Duration
- Methods (ServiceResolver):
Lookup(key string) (HostInfo, error),LookupN(key string, n int) []HostInfoAddListener(name string, notifyChannel chan<- *ChangedEvent) error,RemoveListener(name string) errorMemberCount() int,AvailableMemberCount() intMembers() []HostInfo,AvailableMembers() []HostInfoRequestRefresh()
- Purpose:
Monitoris the cluster-level membership view (join/leave/evict);ServiceResolveris the per-service hash-ring used to route requests to specific nodes (which History node owns shard 42?). TheLookup(key)method is how the Frontend determines which History node to send a workflow execution request to —keyis typicallynamespaceID/workflowIDand 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.
AvailableMemberCountvs.MemberCountcorrectly 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.
ClaimMapperconverts raw authentication info (JWT token, TLS certificate) into TemporalClaims(namespace roles, system role).Authorizertakes the resolved claims and aCallTarget(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
ClaimMapperWithAuthInfoRequiredcompanion 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,TaskManagerall embedCloseable. - The chasm component hierarchy uses embedding:
TerminableComponentembedsComponent;RootComponentembedsTerminableComponent. MutableContextembedsContextto extend the read-only context with mutation capabilities.
Implicit satisfaction#
- Interfaces are overwhelmingly defined by consumers (the History service defines
NodeBackend, whichMutableStatesatisfies; HSM definesEnvironment, whichshard.contextImplsatisfies). This is the Go idiom of interface ownership by the importer. - Authorization interfaces (
Authorizer,ClaimMapper) are defined in a sharedauthorizationpackage intended for operators to implement — provider-owned interfaces for the plugin pattern. //go:generate mockgenannotations on all major interface files signal intent for dependency injection and testability.
Stdlib interfaces used#
- No direct
io.Reader/io.Writeruse in domain interfaces — data is protobuf-serialized to[]byte. context.Contextis pervasive as the first parameter in all persistence and engine interfaces.chasm.Context.Value(key any) anydeliberately mirrorscontext.Context.Valueto make the chasm context feel ergonomically familiar.
Key abstractions#
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.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.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.hsm.StateMachineDefinition+NodeBackend— Together these two small interfaces are the migration seam.StateMachineDefinitionis how new features plug into the tree framework;NodeBackendis how the tree framework reaches back into the legacy mutable state. They represent the architectural joint between old and new.dynamicconfig.Client— The simplest interface and arguably the most practically impactful for operators. A singleGetValuecall 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:
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.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@@@SNIPSTARTmarkers, indicating they are intended for external documentation.Dynamic configuration (
dynamicconfig.Client, optionallyNotifyingClient): Custom config backends (etcd, consul, launchdarkly, etc.) only need to implement the single-methodClientinterface and optionally theNotifyingClientfor push-based delivery.HSM components (
StateMachineDefinition): Features are added to Temporal by registering new state machine types with thehsm.Registry. Thecomponents/directory shows the pattern:callbacksandnexusoperationseach register their own state machine definitions, executors, and event definitions without touching the coreMutableStatecode.
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.